Decide: any space member can grant space content outward #595
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#595
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while writing the #585 docs run, from code — not yet exercised live. Cut as its own issue so the docs don't silently decide a product question.
What the code says
resolveOwned(internal/api/grants.go) gates grant creation on exactly two things: the path must not be delegated (reached through someone else's grant — the re-share refusal), and it must stat in the caller's scope. A member's scope includes/spaces/<name>/..., so a space file passes both checks andPOST /api/v1/grantswill store a grant against the space's actual backend path — created by any member, not the space owner.The dialog offers it too: the row menu's Share entry is gated (since #588) on delegation and the read-only flag, neither of which excludes a writable space path.
Why it might be fine
Spaces members already have the content; a grant is arguably no worse than downloading and re-sharing a copy, and it stays revocable and audited (
share-grantnames the grantor).Why it might not be
resolveOwnedchecks stat, not role, so a read-only member can likely create a grant — possibly even with rolewrite. Whether an entitled write through such a grant is then refused by the grantor's own read-only scope is unverified. If it is not, a read-only member can mint write access they do not have.What to do
/spaces/...file with an outside account; check what the recipient can read and write, and what the audit log records.resolveOwnedis the single place to enforce it, and sharing.md + spaces.md get one sentence each. The #585 docs run deliberately says nothing about space-file grants until this is decided.Code recon done — the sharp version of this is confirmed from source, not just suspected
Reading
internal/storage/scope/shared.gosettles the "unverified" bullets:resolveOwnedchecks only not-delegated + stat-in-scope. A read-only space member's scope stats/spaces/<name>/filefine, andgrantCreatevalidates role againstread|writewith no reference to the caller's own role on the path. So a read-only member can mint awritegrant on space content.resolveSharedsets writability purely from the grant —readOnly: g.Role != "write"— and thenasOwnerswaps the context to the minter's identity with no groups before calling inner drivers. The space-role check lives in the scope layer that has already been bypassed by the overlay's own resolution; nothing downstream re-checks it.asOwner's comment even notes the swap is "inert" for spaces — true for encryption (path-keyed), not for authorisation, where inert means absent.Net: a read-only member of a space can give an outside account write access to space content that the member cannot write themselves. Privilege escalation by way of the grant table. The audit hook does name the real reader and the minter, so it is at least attributable — but attributable escalation is still escalation.
Options
A — Refuse grants on
/spaces/**entirely, for now (recommended). One refusal branch inresolveOwned("space content is shared by space membership — ask the space owner to add them"), one test, one sentence each in sharing.md and spaces.md. Space sharing stays what it already is: membership, managed by the owner, with read/write roles — a working, understood model. Personal files keep grants. This is the smallest rule that closes the escalation, loses nothing that was deliberately designed (space-file grants were never decided — they fell out ofstatsucceeding), and leaves room to relax later. Relaxing a refusal is painless; retracting a capability someone started using is not — the same reasoning as the re-share refusal.B — Owner-only: the space owner (not members) may grant space content outward, role capped at the target's… this immediately runs into "which role does the owner's grant carry when membership already exists", plus the owner's Members dialog still not showing grant-recipients. More rules, still a second sharing system bolted onto spaces.
C — Cap at use-time by the minter's live role: keep minting free, compute
readOnly = grant says read OR minter's current space role is read-onlyat resolve time. Dynamic (demote the member, the grant degrades — elegant), but it means a grant's meaning silently changes with someone else's membership edits, the minter keeps being an invisible conduit the owner can't see, and it needs a role lookup inside the hot resolve path. Closes the escalation, keeps the governance hole.Consequence of A said out loud: someone who today shares a space file with an outsider loses that ability and is pointed at the space owner. On the dogfoods nobody has done this yet (the grant store is one entry old), so there is nothing to migrate — which is exactly why now is the moment to pick the rule.
Live confirmation on the dogfood is possible (mint a grant as a read-only member of Fall 1, watch the outsider write) but given the code path is unambiguous I'd rather spend that effort on the fix's red test, which will encode the same scenario server-side. Your call: A, B, C, or A-with-amendments.
Decided (Option A) and fixed — #600, shipped in v0.6.215, live on both dogfoods.
resolveOwnednow refuses any grant whose resolved backend path lands under/spaces/: space content is shared by space membership: ask the space owner to add them. Same chokepoint as the re-share refusal, one branch.The red test stands as the live confirmation this issue asked for: minting a write grant on a space path as an ordinary member returned 201 with the grant stored before the fix — the escalation was real, recorded in the run log — and returns 403 with an empty store after. With the mint refused,
resolveShared's grant-only writability derivation is unreachable for space paths, closing the read-only-member escalation at its single entrance.Docs shipped in the same PR: sharing.md ("Space content is not grantable") and spaces.md (membership is the grant), both noting public links on space content are unchanged.
Relaxing this later (owner-only grants, or use-time role capping — options B/C on this thread) remains open as a deliberate future decision; nothing to migrate since the grant store never accumulated a space grant outside the test.