Skip to content

feat(authz): server-side @owner injection and audit-column auto-fill#63

Merged
rrrodzilla merged 1 commit into
mainfrom
feat/inject-owner-and-audit-columns
May 26, 2026
Merged

feat(authz): server-side @owner injection and audit-column auto-fill#63
rrrodzilla merged 1 commit into
mainfrom
feat/inject-owner-and-audit-columns

Conversation

@rrrodzilla
Copy link
Copy Markdown
Contributor

Summary

  • Closes the @owner impersonation hole. The Cedar owner_write / owner_restrict policies decide per-record access by comparing resource.<owner_field> == principal.id, but create_entity never populated the owner field server-side. A caller posting {"created_by": "user:victim"} would plant a row under the victim's id. inject_owner_on_create force-sets @owner to the principal id (stripped to match Cedar's principal.id attribute shape) and overwrites any client value.
  • Closes ownership-transfer via update. strip_owner_on_update drops the @owner field from PUT / PATCH bodies so ownership is immutable post-create. Without this, a passing can_modify check could be followed by a write that quietly handed the record to another user.
  • Auto-maintains conventional audit columns. inject_audit_columns_on_create / _on_update fill created_by / created_at / updated_by / updated_at when the schema declares them. Closes the WebhookSubscription.created_by drift from the audit report and gives every operator schema a free, spoof-proof audit trail without per-handler wiring.
  • 11 new unit tests cover the helpers, including the specific impersonation and ownership-transfer regression cases.

Third of four PRs from the production-audit gap review. Follows #61 / #62. PR-D (Cedar engine deny audit) is up next.

Test plan

  • cargo nextest run --workspace --features schema-forge-acton/surrealdb — 1680 passed
  • cargo clippy --workspace --features schema-forge-acton/surrealdb --all-targets — clean
  • New tests: inject_owner_on_create_* (4), strip_owner_on_update_* (2), inject_audit_columns_* (4), incl. the spoof regression

The Cedar @owner policy decides per-record access by comparing
resource.<owner_field> == principal.id, but the create_entity handler
never populated the owner field server-side. A client POSTing
{"created_by": "user:victim"} would plant a row under the victim's id —
impersonation, denial of access to the actual creator, or both. Strip-on-
update was likewise missing: a passing record-level can_modify check
could be followed by a write that quietly transferred ownership.

This change:
- inject_owner_on_create force-sets the @owner field to the principal's
  id (stripped to match Cedar's principal.id attribute shape) on every
  create. Any client-supplied value is overwritten.
- strip_owner_on_update drops the @owner field from any PUT/PATCH body
  so ownership is immutable post-create.
- inject_audit_columns_on_create / _on_update auto-fill conventional
  created_by / updated_by / created_at / updated_at columns when the
  schema declares them. Closes the WebhookSubscription.created_by drift
  reported in the audit and gives every operator schema a free,
  spoof-proof audit trail without per-handler wiring.

Wired into routes/entities.rs at create_entity (owner + create audit
columns), update_entity (strip owner + update audit columns), and
patch_entity (same as update). 11 new unit tests cover the helpers
plus the impersonation regression specifically.

Third of four PRs from the production-audit gap review. D (Cedar engine
audit emission) is next.
@rrrodzilla rrrodzilla merged commit 0c89b23 into main May 26, 2026
1 check passed
@rrrodzilla rrrodzilla deleted the feat/inject-owner-and-audit-columns branch May 26, 2026 21:17
rrrodzilla added a commit that referenced this pull request May 26, 2026
Release rolling up the production-audit gap PRs (#61, #62, #63, #64).

BREAKING (pre-1.0 minor):
  schema-forge-backend 0.11 → 0.12
    AuthStore trait gains required `record_login(username, at)` method.
    Downstream impls must add it.
  schema-forge-acton 0.30 → 0.31
    DynAuthStore trait gains required `record_login` shim.
    `access::filter_entity_fields` now returns `Vec<String>` (dropped
    field names) instead of `()`. Callers binding the return value
    must update.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant