Skip to content

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_session cookie (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:

  1. Identity resolved from connection (cookie or local-client).
  2. Op declares its required permission (e.g., space:hostLink:create).
  3. Lookup: does the calling principal hold a capability matching (resource, permission)?
  4. 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

Source: projects/matrix-3/packages/system-auth/src/index.ts:719-757 for the present-state ownership check pattern.