Skip to content

Link permissions

Links and campaigns use the same private-resource policy. A human owns what they create; readers can inspect it, editors can change/delete it, and managers can also share it. Platform administrator claims do not bypass this policy.

Ordinary personal use

Create a campaign and links as usual. A workspace selector is optional. Omitting it uses the account or organization's internal root; you do not need to create a named workspace. Human resources default to private. A missing or foreign explicit workspace is an error and never falls back to that root.

snug link campaign create --name personal --domain go.example.com
snug link create --campaign-id CAMPAIGN --url https://example.com
snug link access grant --resource-id LINK --user-id USER --role reader

Team workspaces and groups

Create a named workspace when a team needs a separate sharing boundary. Workspace roles are reader, editor, manager and creator; these are shared across services. A creator grant permits creation without granting readership of every resource. The account root's initial administration is limited to the live personal owner or organization Owner/Admin. It does not unlock private resources.

snug access workspace create --name Engineering --visibility private
snug access group create --name Reviewers
snug access group add-member --id GROUP --user-id USER
snug access grant --target workspaces --id WORKSPACE \
  --subject-kind group --subject-id GROUP --role reader
snug link campaign create --name releases --domain go.example.com \
  --workspace-id WORKSPACE --visibility workspace

A workspace resource uses its workspace's grants. A private resource excludes those grants and admits only its owner/direct sharing. Named workspaces inherit parent grants unless inheritance is disabled. Changing the default visibility changes future creation defaults; it does not rewrite existing resources.

snug access workspace policy --id WORKSPACE --inherit-access false --visibility private
snug access revoke --target workspaces --id WORKSPACE \
  --subject-kind group --subject-id GROUP --role reader

Groups are evaluated live. Removing a member removes the group path; an independent direct grant remains. These content groups contain human memberships and are not copied from token group claims. A removed organization member fails the live Auth boundary. When the same user rejoins, surviving ownership and grants apply again.

Independent service accounts

A person's API key still acts as that person. A service account is a different, organization/account-owned identity with separately revocable credentials. Its resources use workspace authority; the key issuer is not their owner.

snug access service-account create --name Publisher
snug access grant --target workspaces --id WORKSPACE \
  --subject-kind service_account --subject-id SERVICE --role creator
snug access service-account issue-key --id SERVICE --name deployment --scope link:write

Key issuance returns the secret once. Store it securely and use it as a bearer credential. Permission roles are stored in the database, not in the key or JWT. Scopes are an additional ceiling: link:read cannot write even when its principal has editor access. Empty scopes grant nothing. Issuing a new key requires a human session with service-account administration rights.

A service-created resource must use workspace visibility. It retains the service creator for attribution, while workspace managers control it. A create-only service can create a campaign without subsequently reading its private management data. Rotate by issuing a second key, updating the caller, then revoking the first:

snug access service-account list-keys --id SERVICE
snug access service-account revoke-key --id SERVICE --key-id OLD_KEY
snug access service-account disable --id SERVICE

Revoking one key leaves other keys and resource custody intact. Disabling the service removes its permission eligibility for every key. These credentials never receive the privileges of trusted internal service tokens.

HTTP equivalents

The shared control plane is under /api/v1/access:

  • GET/POST /workspaces; PATCH/DELETE /workspaces/{id}.
  • GET/POST /groups; PATCH/DELETE /groups/{id}; POST/DELETE /groups/{id}/members.
  • GET/POST /service-accounts; PATCH /service-accounts/{id}.
  • GET/POST /service-accounts/{id}/keys; DELETE /service-accounts/{id}/keys/{key}.
  • POST/DELETE /{target}/{id}/grants, where target is workspaces, links or campaigns.

A shared grant body names one subject and role:

{
  "subject": {"kind": "group", "id": "GROUP"},
  "role": "reader",
  "expires_at": "2027-01-01T00:00:00Z"
}

Subject kinds are user, group, and service_account. Expiry is optional. The existing Link-specific /links/{id}/grants and campaign grant endpoints take user_id and role for direct human sharing. Revocation removes only the named role. Lists use the normal bounded items-collection/page contract for their named collection; filtering and totals retain the authenticated permission boundary.

Public redirect URLs are a separate, narrow capability. They do not grant access to campaign/link management, and password, status, expiry and routing still apply. Human Auth membership and resource transactions use separate stores: an already running resource transaction cannot be atomically cancelled by a Redis membership change. Subsequent requests resolve live membership again.