feat(core,server): element-id ACL — grants that follow a tree-move#291
Merged
Conversation
AclScope { Path | Element } on AclTuple + OpKind::AclGrant. An Element
scope resolves to the element's current path at evaluation via an injected
id->path resolver, so a grant follows the element across an XmlMove; an
unresolvable id is inert (fail-closed). Path scopes are unchanged.
- evaluator (pure + authority tiers) resolves scope->path first, then
composes in the existing deny-overrides / inheritance / provenance
lattice; the provenance walk resolves scope before rooting.
- hand-rolled scope codec (tags 0=Path, 1=Element), total decode.
- STATE_VERSION 10 -> 11; snapshot + zone/read projections resolve scopes.
- AclCursor::grant_element / grant_scoped.
- acl_element.rs: move-safe grant, no stranded restriction, no exfil-by-move,
unresolvable=inert, element scope in rooting/revocation/bounded-deny, codec.
…xt index Inject the room's element_paths index as the id->path resolver at every doc-ACL enforcement seam — the write gate, per-op read redaction (fan-out + catch-up), and snapshot projection — so an element-scoped grant resolves to the element's current path and the verdict follows the element across a tree-move. Snapshot projection derives the resolver from the decoded doc it projects, so it cannot drift. Unresolvable id -> inert (fail-closed). acl_redaction.rs: the three security properties over the redaction seam with real XmlMove (move-safe / no-strand / no-exfil), a whole-doc reader sees all, op-join<->snapshot-join convergence for an element grant, no-ACL move regression.
… element scopes A snapshot projection dropped an element-scoped ACL tuple whose element no longer resolves, while the op-stream gates the same grant/revoke at root — so an op-served root reader kept the tuple and a snapshot-served one lost it, diverging the two catch-up seams. Redact an unresolvable-element tuple by root read in project_read_paths, mirroring op_read_path's root fallback. Regression test drives both seams (root reader keeps, non-root reader drops). Also from the high-effort review: - element_resolver delegates to scope_path (one id->path encode, not two). - write gate + subscribe build the element index lazily (skip the tree walk on a room with no doc-ACL records). - document that an element-scoped grant/revoke gates at the element's current path, so its audience follows the element across a move.
vieiralucas
added a commit
that referenced
this pull request
Jul 15, 2026
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.
Doc-ACL grants keyed by a stable ElementId that resolve to the element's current path at check time, so a grant moves atomically with the element across a tree-move (Kleppmann
XmlMove).Closes the two path-only-ACL holes tree-move opens:
Motivating shapes: a Notion page dragged between parents, a Kanban card across columns, a folder reparent — movable content carrying its own permission.
Core:
AclScope { Path(Vec<u8>) | Element(ElementId) }onAclTupleandOpKind::AclGrant.&dyn Fn(ElementId)->Option<Vec<u8>>) — core hosts no element index. An unresolvable id is inert / fail-closed (roots nothing, governs nothing, denies nothing); aPathscope never consults the resolver.0=Path,1=Element), total decode; snapshot + zone/read projections resolve scopes.AclCursor::grant_element/grant_scoped.Server:
index::element_paths) as the resolver at every doc-ACL seam — write gate, per-op read redaction (fan-out + catch-up), snapshot projection — so element scopes resolve to the element's current path consistently. Snapshot projection derives its resolver from the decoded snapshot it projects, so it cannot drift.Security properties are test-demonstrated over the redaction seam with real
XmlMove(move-safe / no-strand / no-exfil), plus whole-doc reader sees all, op-join↔snapshot-join convergence, and a no-ACL move regression. An unresolvable-element tuple redacts by root read on both catch-up seams so they converge.Pre-release: back-compat / dual-decode /
STATE_VERSION-bump findings are invalid — the format is broken freely,STATE_VERSION10→11, a stale version rejects hard.Summary by cubic
Add element-scoped ACL grants keyed by
ElementIdthat resolve to the element’s current path, so permissions move with content acrossXmlMove. This closes path-only holes (no exfil-by-move, no stranded restrictions) and keeps op and snapshot catch-ups consistent.New Features
AclScope { Path | Element }onAclTupleandOpKind::AclGrant; addedAclCursor::grant_elementandgrant_scoped.0path,1element); bumpedSTATE_VERSIONto 11. Projections resolve scopes; unresolvable element tuples redact by root to mirror op gating.Migration
AclTuple.pathwithAclTuple.scope(AclScope::PathorAclScope::Element); when emitting grants, preferAclCursor::grant_elementfor movable content.evaluate,decide_capability, andeffective_rolesnow take a resolver; pass&|_| Noneif only path scopes are used.Written for commit 491f167. Summary will update on new commits.