Uh oh!
There was an error while loading. Please reload this page.
Granular access control: per-verb permissions, denials, and the _acl block - #1122
Merged
Conversation
Contributor
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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 freeto 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.
Records in an eVault now carry their own access rules: who may read, add, change, or delete them, held inside the record so the policy travels with the data when it syncs.
This implements the list half of the access control design — grants, denials, and the decision order. The Resource Link Ontology half (resolving a platform's score and comparing it) is left as an injected seam, since the scoring side already exists elsewhere.
The problem
Platforms have no access controls between them. Any platform holding a valid Registry-issued token can reach anything that syncs to it, and the legacy
aclarray is all-or-nothing — there is no read-without-write except["*"], which is what essentially every record in the system is written with today.What is here
A policy engine (
infrastructure/evault-core/src/core/acl/) — pure, no I/O:0x01READ,0x02CREATE,0x04UPDATE,0x08DELETE.0x03expresses add-only access. Reserved bits 4-7 are rejected on write and stripped on read;0x00counts as no grant rather than an empty one.requiregroups againstdefault_perms. A named party never falls through from a grant todefault_perms.Persistence — the block is stored as an
aclBlockJSON property on:MetaEnvelope, carried through every read site, both store paths, the update path, and the cross-eVault migration copy. An update that omits_aclpreserves the stored policy rather than clearing it.Enforcement —
VaultAccessGuardnow takes the permission each operation needs; all 21 resolvers are mapped. The consequential change:Delegated identity — a platform's token proves the platform, not which of its users a request is for.
X-ON-BEHALF-OF: @<ename>carries that: the named user becomes the party, with the carrying platform recorded alongside it, so a user grant applies at user specificity while a platform grant still applies at its own.It is the platform's assertion, not a proof — the eVault cannot verify it, so a platform can reach what a user was granted, including more than its own grant. That is the deliberate reading of the design's specificity rule, and a platform that can write to a vault can already act as its users in other ways. What it cannot do is escape a denial: denials match the carrying platform too.
Only
@-prefixed eNames are accepted as parties. This also fixes a real trap:currentUseris derived from the JWTkid, which for a Registry platform token is the literal stringentropy-key-1— a signing-key id, not a party. It is no longer treated as an identity.Reading the policy back —
MetaEnvelope._aclis exposed to anyone permitted to read the record, as typed output (AclBlock/AclGrant/AclDenials/AclCondition). It always reports the policy in force: a record carrying only a legacy array reports the block that array maps to, so clients see one shape regardless of how the record was written. The legacy array itself is still never returned.Note this means a denial list is visible to permitted readers — it names the parties an owner excluded.
GraphQL input —
AclBlockInputand friends, with_aclonMetaEnvelopeInput,BulkMetaEnvelopeInput, andUploadFileInput.Nothing narrows until an owner sets a policy
Every platform writes
acl: ["*"]today, so back-compat mattered more than purity here. Records with no_aclkeep their exact current behaviour, token bypass included. Legacy arrays are read as:["*"]→default_perms 0x0Fbehind an always-passing group;["@ename"]→ a0x0Fgrant to that eName. Binding documents (acl: [subject]) are unaffected.Docs
W3DS Protocol/Access-Control.md— the protocol model, wire format, decision order, the delegation header, and reading policies back.Post Platform Guide/access-control.md— the implementation guide: worked policy shapes, thepermstable, header usage, and the failure modes most likely to bite (a grant is final;0means no grant, not "denied"; a platform token will not open a policied record; policies replace rather than merge).Infrastructure/eVault.mdrewritten — it previously documented the old model as a limitation with granular permissions "planned for future versions".skills/w3dsreference carried the same stale claim and is corrected; it is symlinked into installed agent skills.Deliberately not in scope
principalFor.requiregroup containing conditions cannot pass, and a deny condition always fires. Policies should usegrants,denials.enames, and empty-grouprequireuntil one lands.acl: ["*"]with no_aclparameter and does not sendX-ON-BEHALF-OF, so records written throughhandleChangecannot carry a policy or a delegated identity yet — a direct GraphQL call is needed. Platforms do not evaluate policies themselves.Deploying
No migration. Neo4j is schemaless and
aclBlockis a new optional property, so existing nodes take the legacy path untouched. Nothing can write a policy through the adapter yet, so the change is inert on deploy.One caution: treat this as forward-only once policies start being written. Rolling back means old code ignoring
aclBlockand reading theaclarray, which platforms set to["*"]— a locked-down record would become world-open again.Testing
30 engine tests written from the design's normative examples, plus 19 guard integration tests against real Neo4j covering the token-bypass closure, specificity, deny-over-grant,
default_perms, fail-closed conditions, delegated identity and its denial escape-hatch, rejection of non-eName identities, and legacy records staying unchanged. Full suite: 213/213 across 18 files. Typecheck clean; docs build passes withonBrokenLinks: 'throw'.https://claude.ai/code/session_01UpwygDu2cizLp12tvvKqVZ