SPEC 03 — Semantic Model: Permissions Registry v0.2
File: specs/03-permissions-registry.md
Status: Normative
Version: 0.2
Purpose
Define a deterministic “Permissions Registry” semantic layer that materializes access control state from ledger entries.
This layer mirrors the “permissions table” concept:
- Grants are rows
- Revokes are rows (optional but included in v0.2)
- Effective capabilities are computed by replay
This layer depends on Identity Registry only for convenience (e.g., tooling), not for correctness of can().
Core Concepts
Scope
A string namespace, e.g.:
- projects:alpha
- journal:sam:2026
Capabilities
Allowed capability values:
- read
- write
- grant
- admin
Implications:
- admin implies grant, write, read
- grant implies read
Targets
v0.2 supports principal targets and groups.
Target forms:
- { “type”: “principal”, “id”: “did:key:…” }
- { “type”: “group”, “id”: “group:…” }
Bootstrap
Permissions requires initial authority.
Bootstrap rule (Normative for v0.2):
- Tooling MUST accept a configuration array rootAdmins: principalId[]
- rootAdmins are treated as having admin for all scopes
Example:
{
"rootAdmins": ["did:key:alice..."]
}
Groups
Entry Kind: group.upsert
Payload:
{
"groupId": "group:engineering",
"displayName": "Engineering"
}
Rules:
- First group.upsert for a groupId sets group owner = entry.author
- Subsequent group.upsert may only be authored by group owner or rootAdmin
Entry Kind: group.member.add
Payload:
{
"groupId": "group:engineering",
"principalId": "did:key:..."
}
Entry Kind: group.member.remove
Payload:
{
"groupId": "group:engineering",
"principalId": "did:key:..."
}
Rules for membership changes:
- Only group owner or rootAdmin may add/remove members
- Membership is effective from the entry forward (replay-ordered)
Grants and Revokes
Entry Kind: perm.grant
Payload:
{
"scope": "projects:alpha",
"cap": "read",
"target": { "type": "principal", "id": "did:key:bob..." },
"constraints": {
"expires": "2026-06-01T00:00:00Z",
"note": "optional"
}
}
Rules:
- author must have effective grant or admin for scope at the point just before applying this entry
- cap must be one of: read, write, grant, admin
- constraints.expires, if present, is ISO-8601
- Grants accumulate (union)
Entry Kind: perm.revoke
Payload:
{
"scope": "projects:alpha",
"cap": "read",
"target": { "type": "principal", "id": "did:key:bob..." },
"reason": "optional"
}
Rules:
- author must have effective admin for scope at the point just before applying this entry
- Revokes take effect from that point forward
- A later perm.grant can re-enable
Effective Capability Computation
To compute effective caps for a principal in a scope at some replay point:
- Start with implied caps from rootAdmins if principal is in rootAdmins.
- Expand group membership: principal is member of groups if group.member.add without subsequent remove has occurred.
- Collect all active grants in scope:
- Direct grants to principal
- Grants to groups the principal is a current member of
- Remove expired grants (if constraints.expires is in the past relative to “nowIso” parameter; if no nowIso supplied, treat as non-expiring for deterministic verification mode)
- Apply revokes:
- Any revoke matching (scope, cap, principal) disables that cap from that point forward, unless re-granted later
- For group-targeted grants, revoking a principal does not alter group membership; it only affects effective caps.
Implied caps:
- admin ⇒ grant, write, read
- grant ⇒ read
Authorization API (Normative)
Permissions tooling MUST expose:
- getEffectiveCaps(state, principalId, scope, nowIso?) → Set
- can(state, principalId, action, scope, nowIso?) → boolean
Default action mapping:
- perm:read requires read
- perm:write requires write
- perm:grant requires grant
- perm:admin requires admin
Applications may map their own actions to caps.
Validation Approach (Normative)
Replay must validate authorization “just before apply”:
- When processing perm.grant, check can(author, perm:grant, scope)
- When processing perm.revoke, check can(author, perm:admin, scope)
This requires single-pass replay that maintains current state and validates each entry in order.
Determinism Modes
Two modes are allowed:
Deterministic mode (default for consensus checks):
- Ignore expires unless nowIso is explicitly provided by the caller
- This makes replay purely ledger-derived
Operational mode (for live systems):
- nowIso is set to real current time and expires are enforced
Tooling MUST document which mode is used.