Problem
Seed currently uses document paths for several concerns: identity, placement in a hierarchy (ontology), and permissions. Moving a document can therefore change identity, break links or require permanent redirects, and change access at the same time.
For this reason we kept our private documents implementation very simple — by not allowing them to be nested. But users obviously want to organize their documents, and they expect some kind of inherited permissions idea like in Notion or Google Drive.
Solution
As always in software — many problems can get solved by adding another layer of indirection. So we segregate the different concerns paths carry into separate layers.
Authority and Permissions
Permissions were the first major challenge. Users expect one workspace, replaceable admins, many documents, and granular sharing. Inheritance — “anyone who can access Cars can also access Toyota” — becomes especially tricky when peers can change permissions independently while offline.
After a few weeks exploring prior art, the WIP Keyline proposal from Ink & Switch looks like a promising foundation.
Keyline
Keyline represents authority as a graph. Nodes are things — people, documents, groups, etc are all nodes. Nodes have some authority, at least over themselves, and can delegate authority to other nodes. Edges are directed signed delegations representing the flow of authority over a subject at some access level which are labeled on the edge.
Access levels are simple and ordered: Sync < Read < Write < Admin (Sync and Read are different only if we use encryption).
This is still a capability system — any holder can further delegate their granted access with the same or lower access level. Access composes through the graph, capped at the lowest level along the route.
For example: Doc can grant Admin over itself to Workspace, which can delegate further Write to Alice, who can delegate it further to Bob as Read.
Delegations are late-bound: they work while their issuer has sufficient authority, stop working when that authority disappears, and can recover when another route restores it. Peers holding the same grants and revocations compute the same result, regardless of arrival order.
Delegations can be revoked. Revocation is a signed statement pointing to a delegation by hash, allowing to cut paths in the graph.
Issuers can revoke anything they've issued — Retraction.
Delegates can revoke anything they've received — Renunciation.
Any node in the graph can revoke a delegation on the path that starts from their own node.
Admins can revoke anything that starts from a node they are admin of.
Admins are special — they retain their revocation power over their admin reach forever.
The state of the system is evaluated by two separate calculations:
Positive pass: built the graph ignoring all revocations, calculate admin reach.
Live pass: process revocations and determine live edges.
Admin Rotation
How do we revoke an admin then? By cutting the "supply" of authority to them, and creating new paths to the admins we want.
TODO: outdated content below this line.
Stable IDs
Our initial idea was to separate stable identity of resources (nodes) from their mutable location:
A stable NodeID identifies a node within a space.
A node has one canonical parent plus an optional name within that parent.
Paths and breadcrumbs are derived from canonical locations.
Capabilities target stable nodes (not paths), and apply either to that exact node or to its whole subtree, which is the default.
Permanodes
I bring back the idea of a Permanode. I don't care what we call it though.
Permanode establishes a new Group (and also a Resource, because resources are groups too). It doesn't need to be signed, but it has to declare its owner. Permanode is useless until further signed elsewhere by the declared owner.
Roughly the model could look something like this:
type Permanode = {
nonce: Bytes
owner: PublicKey
ts: Timestamp
}
type GroupID = CID<Permanode>
type Delegate = PublicKey | GroupID
type Capability = {
// Permanode is the start of the capability chain.
// If permanode is the proof, this capability must be signed
// by the declared owner.
proof: CID<Capability> | GroupID
delegate: Delegate
access: AccessLevel
ts: Timestamp
signer: PublicKey
signature: Signature
}
// Waterfall authority from most powerful to least powerful.
// Sync level is there for the future when we have E2EE.
type AccessLevel = "admin" | "write" | "read" | "sync"
type Revocation = {
capability: CID<Capability>
proof: CID<Capability>
ts: Timestamp
signer: PublicKey
signature: Signature
}Space Group
Spaces are Groups too. For compatibility with existing data with can say that the root of the Space's Group is a deterministic Permanode that can be derived from the space's public key alone.
All existing Capability blobs can be assumed to have this Permanode in the proof field (because we didn't implement delegations yet, only space owner is able to issue caps).
Transitive Permissions
This model allows to express transitive permissions. It's important to note that we only talk about authority hierarchies for now, not the actual hierarchies of documents and names. Just the authority graph.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime