Repository navigation
feat(authz): authorization foundation (RBAC slice 1) — CRUD enforced, events pending - #71
Open
phoebus-84 wants to merge 26 commits into
Open
phoebus-84 wants to merge 26 commits into
phoebus-84 wants to merge 26 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
controller_for/3's catch-all let an :anonymous principal become the resource
controller via the declared primary_accountable_id, contradicting the module
doc ("never the declared receiver alone") and the deny-by-default policy for
anonymous callers. multi_on_create/3 now authorizes resource.create for
:anonymous contexts before assigning control, aborting the whole multi
(event + resource + control all roll back) on deny.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eadlock Concurrent revokes/updates of two different system-role relationships in the same organization could deadlock (Postgres 40P01): each transaction locked its own target row FOR UPDATE before reaching the organization's advisory lock, taken only once Membership.multi_revoke's authorize step ran (which also locks the actor's own admin row FOR SHARE). Two such transactions could each hold one row lock while waiting on the other's. Fix: AgentRelationship.Domain.multi_load/3 now peeks at the target row without locking it, and if it is a system-role relationship, takes Membership.lock_org/2 (now public) before locking the row FOR UPDATE, so every transaction touching an org's system relationships acquires locks in the same order. Added test/authz/concurrency_revoke.test.exs: a deterministic handshake test plus a genuinely unsynchronized concurrent-race test (verified to reproduce the pre-fix deadlock reliably, and to never raise against the fix). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ganizationAdmin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ution Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The line-based regex missed a multi-line %ExecutionContext{...request_id: ...} literal split across lines. Scan each source file's full content instead, via an extracted context_literal?/1 helper covered by its own unit test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nator ruling Assert errors != [], each error matches FORBIDDEN, and the resource is unchanged, instead of splitting into three requests. Absinthe still collapses the three sibling non-null field errors into fewer entries in the response, but the unchanged resource state plus the all-FORBIDDEN check together prove every resolver was independently denied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- api.test.exs: assert Alice's organization name and Alice's own name are unchanged after Bob's denied updateOrganization/updatePerson calls, not just the FORBIDDEN error shape. - concurrency.test.exs: wrap the post-assert_receive assertions in try/after so the revoker task always gets :commit, even if an assertion fails; otherwise it stays parked in its receive holding row locks and on_exit cleanup can hang. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Facts.admin_of/3 now takes the organization ids a decision needs and locks FOR SHARE only the actor's admin rows of those organizations, instead of every org.admin row of the actor. Membership.multi_grant takes the organization's advisory lock before loading admin_of, the same order as multi_revoke. Together this removes the same-org grant-vs-revoke deadlock and the cross-org mutual-revoke deadlock. Adds deterministic and repeated-trial race tests for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…content - guard: ExecutionContext.admin_key/1 is only called from the three admin-key resolvers. - GraphQL: an unsigned updateEconomicResource with allow_unchecked off is UNAUTHENTICATED and leaves the resource unchanged. - Permission log rows of grant/revoke/resource create+delete/person create+delete carry only the allowed before/after keys and no public keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AgentRelationship.system_changeset/2 requires in_scope_of == object. - Backfill test checks the logged controller_id. - Spec: §10 has no partial unique index, §3.4 names Log.multi_insert/4 and insert/3, §11 describes the pending-mutation allowlist, §14 delta 13 describes scoped admin_of locking and the grant lock order. - Release notes: hard-delete consequences of revoke-only grants, backfilled organization-controlled resources, rollback deleting grants, TRUNCATE not blocked by the trigger; drop the fixed cross-org deadlock limitation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
phoebus-84
had a problem deploying
to
test_trace
October 1, 2026 15:47 — with
GitHub Actions
Failure
This was referenced Oct 2, 2026
This was referenced Oct 9, 2026
This was referenced Oct 12, 2026
This branch had an error being deployed
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.
Summary
First authorization slice for Zenflows: CRUD enforced; events pending. It fixes the architecture review's F01 finding (a valid signature did not imply the right to the object) on resource, organization, relationship, role and person CRUD.
ExecutionContextbuilt from the verified signer (req_user), the admin key, or fixed-scope service accounts, and passed into domain functions.Zenflows.Authz.Policy, evaluated inside the sameEcto.Multitransaction as the write, on facts loaded with row locks.zf_resource_control: administrative control of a resource, kept separate from ValueFlowsprimaryAccountable/custodian. Existing resources are backfilled fromprimaryAccountablewithsource: backfill.AgentRelationships with seeded system roles (org.admin,org.member, scoped byin_scope_of):revokedAt), never deleted.zf_permission_log(a DB trigger rejectsUPDATEandDELETE), written in the same transaction as every permission change.FORBIDDEN/UNAUTHENTICATEDerror codes;resourceControlquery;assignOrganizationAdminmutation;AgentRelationship.inScopeOf/revokedAtandAgentRelationshipRole.systemKey.Design and rationale:
docs/authz-foundation-design.md(see §14 for how it differs from the original draft). Release notes, including behaviour changes, operations steps and known limitations:docs/authz-release-notes.md.Before deploying
assignOrganizationAdminbefore users need to edit them.createEconomicEventcan still affect another agent's resource.AUTHZ_ENFORCEandAUTHZ_ALLOW_UNCHECKED. The server refuses to start in production with either one set to its permissive value.Test plan
Full suite: 684 tests, 7 failures, none caused by this branch:
mastertoo:restroom.test.exs:25,35,41andeconomic_resource/domain.test.exs:81.zenflows-cryptoimage:person/type.test.exs:94,155(already failing onmaster) and the newauthz/organization.test.exs:79.Alice/Bob negative tests at domain and GraphQL level, including aliases and several root fields in one document.
Concurrency tests:
The race trials hit Postgres deadlocks (40P01) on the earlier code and pass now.
Migrate, roll back and re-migrate on a fresh database; every seeded resource gets a backfilled controller.
Run
test/authz/organization.test.exsandtest/vf/person/type.test.exsagainst a working Restroom/zenflows-crypto (admin-key path).🤖 Generated with Claude Code
Cross-repository integration
Workstream: #74
Shared label:
workstream:authz-integration. This is a non-closing backlink; merging one slice must not close the overall integration.