Appearance
Space Authorization Model
This document defines how Soma authorizes reads and writes of space-scoped content (documents, messages, blobs).
What counts as a member?
Possession of a valid MembershipCapability for the target space_id:
- Signed (
signedpresent). - Subject matches the requester’s libp2p
PeerIdfrom the authenticated channel. - Not expired (
expires_atis in the future, if set). - Issuer is trusted:
- Issuer is the space owner, or
- Issuer presents a valid
IssuerCapabilitydelegated by the owner for that space.
Invariant: all space reads require membership
Every surface that returns space content must check membership:
- libp2p request/response (
/soma/blob/1and any future protocols). - In-process daemon surface exposed to the renderer via the Tauri host (
desktop-apihandlers →desktop-commands→@soma/sdk). somad botadmin HTTP (if it ever exposes read APIs).
Controllers are thin; the check should live in the service layer so all surfaces reuse it.
Where membership is enforced today
- libp2p blobs: blob handler calls
SpaceAuthorizerbefore serving bytes. - In-process daemon (via the Tauri command surface):
uploadBlob/upsertDocumentrequire membership. - Space creation:
CreateSpacestores owner metadata plus an owner-signedSpaceGenesisArtifact. - Join decisions: membership capabilities are signature/subject/issuer-checked before persistence; delegated issuer chains are verified when the owner key is known.
- Issuer delegation:
IssueIssuerCapabilityis implemented on the in-process daemon surface, requires the current daemon to own the space, and persists the owner-signed delegation locally. - Discovery:
DiscoverSpacesis implemented as local known-space discovery. It does not query rendezvous metadata yet.
Verification rules
- Verify signatures with the signer’s libp2p public key (from Identify).
- Bind
subject_peer_idto the remote peer identity (not payload). - Enforce expiry on membership and issuer capabilities.
- Delegation chain: owner signs issuer cap → issuer signs membership cap.
- Reject signed capability payload mismatches so post-signature field tampering does not persist.
Space invites
A soma://invite/<payload> link is an owner-signed, self-contained credential — the mechanism soma_membership::invite implements. It closes part of the TOFU gap described above: without an invite, the only input to a first-contact trust decision is a peer id typed or pasted out of band, with zero cryptographic binding to the space or to whoever handed it out. An invite instead binds (space_id, default_role, expires_at, bootstrap_multiaddrs, issuer) into one owner-signed unit, verifiable before dialling anyone.
- Wire format: CBOR (via
ciborium) of the full signedInviteState, base64url-encoded (no padding), assoma://invite/<payload>. Seesoma_common::invite_linkfor the exact byte layout. - Offline verification:
CborSigned.signer_public_key(protobuf-encoded libp2p public key) is embedded directly in the invite — the one signed artifact in this schema that carries its own key, because this deployment's real identities are ECDSA (soma_net::NetIdentity::generate), whose peer ids do not embed the public key the way small Ed25519 keys can. Every other signed artifact (MembershipCapability,IssuerCapability,SpaceGenesisArtifact) is still verified exclusively throughPeerKeyResolver/TrustAnchoragainst a locally-pinned key —signer_public_keyis populated only forInviteStateand must never be read by any other verifier. Seesoma_common::verify_invite_state's doc comment for the precise, narrow claim this embedding does and doesn't prove. - Redemption pins the trust anchor:
soma_membership::invite::redeem_inviteroutes the verified issuer through the exact sameresolve_trust_anchor/pin_trust_anchorprimitives as every other first-contact path (never a parallel mechanism), and caches the issuer's verified public key intopeer_public_keysso a later inboundJoinDecisioncan be signature-checked without waiting on a separate Identify exchange. - What is still TOFU: the first time a device sees a given space, trusting that a particular invite link genuinely came from the expected owner is inherent to any invite-link system (Slack/Discord/Docs share links have the same property) — the improvement is that tampering with the link's terms in transit, or a MITM substituting a different issuer/role, breaks the signature and is rejected before any network contact, rather than being undetectable the way a bare pasted peer id is.
- Replay protection: issued invites and per-invite redemption counts are persisted (
invitestable /soma_storage::invites::InviteRepository);try_consumeis a single guardedUPDATEso a single-use invite (the default) cannot be redeemed twice even under concurrent attempts. Decider-side auto-approval (join_decider::storage) trusts only its own previously-insertedinvitesrow, looked up by nonce — never wire-provided invite fields — matching the same "local ground truth only" pattern as bot self-recruitment.
Notes and gaps
- Owner public key discovery is required to validate delegations where owner ≠ sender.
- The signing payload uses the current CBOR view helpers; canonical CBOR remains a cross-version interoperability task.
- UI guardrails should redirect non-members from space routes, but the backend is the security boundary.
- Future work: revocation + key rotation, and optional end-to-end encryption by space.
- Invite
InviteProofrequester signatures are built (soma_common::build_invite_proof) but not yet verified server-side against the requester's own key — doing so needs the requester's public key, which this crate only ever sources from a prior Identify exchange (PeerKeyResolver), and nothing currently guarantees Identify completes before aJoinRequestarrives on a freshly-dialled connection. Auto-approval instead relies entirely on the decider's own locally-issuedinvitesrow (space match via the composite key, expiry, single-use nonce) — seesoma_membership::invite's module doc comment.
Terminology note
Owner,Editor,Viewer,Member, andBotare workspace access roles.- Bot operating mode like
adminis deployment/runtime terminology, not a human workspace role. - Local AI model feature hints in the desktop UI are not security capabilities and do not grant access.
- A delegated approver is a separate authority concept. A bot may be a delegated approver, but the
Botrole alone does not imply join approval powers.