fix(cedar): scope the @owner forbid to mutations so reads follow @access - #156
Merged
Conversation
The generated `forge.<schema>.owner_restrict` policy listed Read and List alongside Update and Delete. A Cedar forbid overrides every permit, so the annotation silently revoked the schema's own `@access(read: [...])` grant for every record the caller had not personally created, with only platform_admin exempt. Any shared workspace built on an @owner schema became per-user silos. A required @owner field made it worse: `build_resource_placeholder` fills required fields with type defaults, so the schema-level placeholder carried `owner = ""`, satisfying the forbid's `resource has "<owner>"` conjunct and denying the whole collection with a 403. An optional owner field on an otherwise identical schema answered 200 with zero rows instead. Scope the policy to Update and Delete. Who may see a record is decided by `@access(read:)` alone; a schema wanting owner-only reads says so with a custom Cedar policy, and the migration note shows how. Everything @owner is relied on for is unchanged: inject_owner_on_create still stamps the creator, strip_owner_on_update still makes the value immutable, and the owner_write permit still lets a creator update their own record. Two generator regressions cover the shape directly, and the three integration tests that asserted the old behavior now assert reads succeed while the non-owner write and delete stay refused. Fixes #151 Refs #155
…hange The ignored PostgreSQL HTTP case asserted 403 on GET for a record owned by someone else, which is the behavior this branch corrects. Keep PUT, PATCH and DELETE refused before the conditional header is honored, and assert the read succeeds while its permissions block reports update and delete as false.
…ntext #155 landed `context.resource_is_placeholder`, so the migration recipe for restoring owner-only reads can now scope its forbid to concrete records. Guard the example with it and scope it to Read, because a collection request preflights the Read action at schema scope before checking each row. Without the guard the hand-written policy reintroduces exactly the 403 this release fixes.
Contributor
Author
|
Merged Two follow-ups from #155 landing, since
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #151.
The defect
@ownergenerates aforge.<schema>.owner_restrictforbid whose action list coveredReadandListas well asUpdateandDelete. In Cedar aforbidoverrides everypermit, so the annotation silently revoked the schema's own@access(read: [...])grant for every record the caller had not personally created. Onlyplatform_adminwas exempt. Any shared workspace built on an@ownerschema became per-user silos, and the schema text gave no hint that it had happened.A required
@ownerfield made it worse.build_resource_placeholder(authz/adapters.rs:292) populates required fields with type defaults, so the schema-level placeholder used for List and Create carriedowner = "". That satisfied the forbid'sresource has "<owner>"conjunct, the collection-level check was denied, and the endpoint answered 403. An optional owner field on an otherwise identical schema answered 200 with zero rows. Same annotation, two different failures, neither of them what the schema says.Found in the FEMA CMDS demo workspace, where the set of dead screens turned out to be exactly the set of schemas carrying
@owner(NewsRelease, Campaign, DistributionList, Distribution) and every schema without it worked. Bastion (schemas/project.schema:owner_id: text required @owneralongside@access(read: ["member","pm","finance","admin"], ...)) is broken the same way — amembercan read zero projects.The fix
Scope
owner_restricttoUpdateandDelete:Who may see a record is now decided by
@access(read:)alone. Everything@owneris actually relied on for is unchanged:inject_owner_on_create(access.rs:446) still stamps the creator.strip_owner_on_update(access.rs:475) plusroutes/entities.rs:3183still make the value immutable after create.owner_writepermit still lets a creator update their own record.tenant_guardstill forbids cross-tenant reads — its action list intentionally keepsReadandList.Cedar remains the sole visibility gate; there is no query-layer ownership filter to keep in step.
Because the placeholder resource no longer participates in an owner decision,
requiredand optional@ownerfields now behave identically. The broader hazard — that a hand-written policy readingresource has "<field>"against a placeholder is comparing against a fabricated default — is filed separately as #155.Behavior change
This widens read access relative to previous releases, so it is written up under
### Security / Breakingwith a migration note. A deployment that was relying on@ownerfor privacy, deliberately or not, will see those records become readable by the roles its own schema already granted. The migration note gives the custom Cedar policy that restores owner-only reads explicitly.Tests
Two generator regressions in
policy_gen.rs:owner_restrict_covers_mutations_only_and_never_reads—UpdateTask/DeleteTaskpresent,ReadTask/ListTaskabsent.owner_schema_keeps_its_read_permit_intact— arequired @ownerschema with@access(read: ["viewer","platform_user"], ...)still emits itsread_viewerpermit, and no owner-derived forbid covers a read.Three integration tests asserted the old behavior and now assert the corrected one, each keeping its original point:
auth_demo::public_reads_enforce_record_forbids_and_ownership— a custom record forbid still hides a row; the owned notice is now readable, and itspermissionsblock reportsupdate: false, delete: false.auth_demo::demo_all_auth_layers_combined— the non-owner manager now reads the document and is refused onPUTandDELETE.conditional_entities::conditional_delete_preserves_owner_denial_without_hiding_the_record(renamed) — the owner denial onDELETEstill precedes conditional-header parsing, and theGETthe schema grants succeeds.Gates
cargo nextest run --workspace --exclude schema-forge-mssql— 2481 passed, 8 skipped, includingfull_policy_set_validates_against_generated_schema(strict-mode Cedar bundle validation).cargo clippy --workspace --exclude schema-forge-mssql --all-targets -- -D warnings— clean.cargo fmt --all -- --checkfails onmaintoday at 329 pre-existing sites (local rustfmt applies 2024 style edition to committed 2021-era formatting). Verified per-file that this branch introduces no new drift: the touched files have the same drift sites before and after (policy_gen 4, auth_demo 3, conditional_entities 7), at shifted line numbers only. Reformatting 79 files does not belong in this PR.--workspacewithout--exclude schema-forge-mssqlcannot build on any branch:schema-forge-mssqlandschema-forge-surrealdbunify intocompile_error!("Features mssql and surrealdb are mutually exclusive.")in acton-service 0.43.0. Confirmed pre-existing by stashing this change and re-running.Docs
docs/query-api-reference.md§12 bullet 3 said record-level visibility is what@ownerdoes. Rewritten to say that every returned row is authorized individually after the query runs, that@ownerestablishes who created a record and stops anyone else changing it, and that owner-only reads need a custom Cedar policy.