Appearance
Security: Permissions
Status: target state. The present-state permission model is a single check per op:
calling.principalId === resource.ownerPrincipalId.
Today's enforcement
Each op reads identity from the actor invocation context (via auth.identity.resolve) and either:
- Allows local-client unconditionally (loopback bypass).
- Allows the resource owner.
- Denies everyone else.
Examples:
ts
// auth.hostLink.create — ownership check
if (!space || space.ownerPrincipalId !== principalId) {
return { ok: false, error: 'Host link requires a Space owned by the principal' };
}
// auth.space.default — same shape
if (!space || space.ownerPrincipalId !== principalId) {
return { ok: false, error: `Space not found for principal: ${nextSpaceId}` };
}There is no:
- Per-op permission grant (e.g., "this principal can read but not write").
- Time-bounded grant.
- Delegation.
- Cross-principal grant.
Target permission model
A capability-shaped model (per ARCHITECTURE-SCOPED-CAPABILITY-RESOLUTION.md):
ts
interface Capability {
id: string;
granteePrincipalId: string;
grantorPrincipalId: string;
resource: { kind: string; id: string }; // e.g., {kind:'space', id:'spc_xxx'}
permissions: string[]; // e.g., ['read', 'list']
expiresAt?: string;
revokedAt?: string;
delegatable?: boolean;
}A capability mints once, can delegate (with permission), and can revoke. Every op checks requireCapability(callerCaps, resource, perm) rather than reading the resource owner.
Permission strings
A target permission lexicon, per resource kind:
Space
space:read— read Space record.space:write— modify Space metadata.space:hostLink:create— pair a Device into the Space.space:hostLink:revoke— disconnect a Device.space:namespace:claim— claim a public path.space:namespace:release— release.space:transfer— transfer ownership.space:suspend— suspend.
Principal
principal:read— read profile.principal:credentials:list— list (no values).principal:role:assign— set role.principal:suspend— suspend.
Runtime
runtime:read— read state.runtime:start,runtime:stop,runtime:reload,runtime:restart.runtime:declare— register new.
Audit
audit:read:own— read own audit events.audit:read:any— read any (security-auditor).
A target role-to-permission mapping:
user→ permissions on resources where they are owner.platform-admin→ all permissions, all resources.security-auditor→ all:read*permissions, all resources.
Workaround for partial access today
If an admin needs to give someone limited access today:
- Cannot grant per-resource read-only access.
- Cannot grant temporary access.
- Workaround: share an
mx_sessioncookie (anti-pattern, anti-policy).
The lack of partial access is a real gap. Security-conscious teams should defer publishing sensitive Spaces on the platform until the capability layer is in place, or rely on out-of-band processes (encrypted file shares, ticketing systems).
Capability resolution path
A capability check at op time:
- Identity resolved from connection (cookie or local-client).
- Op declares its required permission (e.g.,
space:hostLink:create). - Lookup: does the calling principal hold a capability matching
(resource, permission)? - If yes, allow. If no, deny.
The lookup needs a per-principal capability index. The grant/revoke ops must update the index. The expiry path needs a periodic prune.
This is roughly the work of one focused workstream. None of it exists today.
See also
- Security: Authorization — broader model.
- Security: Roles — RBAC layer (target).
- Devices / Device grants — Device-scoped scopes (today partial).
- ARCHITECTURE/ARCHITECTURE-SCOPED-CAPABILITY-RESOLUTION.md — design.
Source:
projects/matrix-3/packages/system-auth/src/index.ts:719-757for the present-state ownership check pattern.