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