Permissions
The permission, not the role, is what the backend enforces.
View as MarkdownAuthorization reduces to one question on every request: is the required permission in this principal's permission set?
Roles exist, but only as a way of bundling permissions onto a membership. The backend reads
the permissions claim and never reads role. That means a custom role you invent is
enforced correctly with no change on our side, because we never knew about roles in the first
place.
The catalog
27 permissions, in ten families. This list is the whole surface.
| Family | Permissions |
|---|---|
| Workspaces | workspaces:read, workspaces:write |
| Members | members:read, members:write, members:invite |
| Roles | roles:read, roles:write |
| Workers | workers:read, workers:write, workers:revoke |
| API keys | api-keys:read, api-keys:write, api-keys:revoke |
| Billing | billing:read, billing:write |
| Backups | backups:read, backups:write |
| Destinations | destinations:read, destinations:write |
| Connections | connections:read, connections:write |
| Artifacts | artifacts:read, artifacts:upload, artifacts:confirm, artifacts:download, artifacts:delete |
| Audit | audit:read |
You can read the live list from the API rather than trusting this page:
curl -fsS https://api.saved.sh/v1/permissions -H "Authorization: Bearer $TOKEN"Why backups and artifacts are separate families
Because "can configure what gets backed up" and "can download the data" are different powers, and a great many people should have the first without the second.
backups:write lets someone change a schedule or add a source. artifacts:download lets
them walk out with the contents. Splitting the families is what makes a read-only operator
role, or a CI key that can trigger but not exfiltrate, expressible at all.
Artifacts are split five ways for the same reason
| Permission | Power |
|---|---|
artifacts:read | See that an artifact exists, its size, checksum and key ID |
artifacts:upload | Request a presigned upload URL for a run |
artifacts:confirm | Declare an upload durably stored |
artifacts:download | Get the bytes |
artifacts:delete | Remove one |
A worker needs the middle two and must not have the others. A monitoring integration needs
the first and nothing else. Collapsing these into artifacts:write would make both of those
impossible.
Roles
| Role | Carries |
|---|---|
admin | Every permission in the catalog |
member | Nothing, by default |
| Custom roles | Any subset you choose |
A bare member has no access at all. That is deliberate: a new member is not
accidentally granted anything by existing. Assign them admin or a custom role, or they
will see an empty workspace and wonder why.
The workspace creator is assigned admin. Custom roles are created in the dashboard, are
self-serve, and need no deploy from us: a membership may hold several roles, and the effective
permission set is their union.
Machine limits
Some permissions are refused on machine credentials no matter what you ask for.
Human-only
| Permission | Why a machine may not hold it |
|---|---|
workers:read, workers:write, workers:revoke | A compromised key could mint further worker credentials |
api-keys:read, api-keys:write, api-keys:revoke | One leaked key could mint more, and revoking the original would not revoke what it made |
billing:read, billing:write | The workspace's cards |
destinations:write | Linking a bucket hands over a long-lived key pair, and skip_permanent pointed at an attacker's bucket redirects every future artifact |
connections:write | The OAuth flow ends at a browser consent screen. A machine could only start a flow it cannot finish |
destinations:read and connections:read return metadata only, never a credential or token,
and remain available to machine keys so automation can render or reconcile a delivery plan.
This is enforced at two layers, not one. A human-only slug is rejected when a key is created, and stripped from the principal on every validation. The second layer exists because creation-time validation cannot reach a key minted before the gate existed, or edited directly in the identity provider.
Worker-only
artifacts:confirm is refused on an API key. Confirming an upload is a worker's own signal
about its own work: an admin can trigger a backup, but never confirms one. The orchestration
hub also requires this permission to admit a worker at all, which makes it the
worker/automation discriminator.
The two-permission worker key
A worker key carries exactly artifacts:upload and artifacts:confirm. It cannot read
backup definitions, list artifacts, download anything, or manage workers, including itself.
That is the whole reason a worker is safe to run on a machine you would not trust with an API key.
Designing roles
Some shapes worth copying.
| Role | Permissions | For |
|---|---|---|
| Read-only auditor | workspaces:read, backups:read, artifacts:read, audit:read | Compliance review with no ability to read data |
| Operator | The above plus backups:write, workers:read | Runs the backups, cannot exfiltrate |
| Restorer | backups:read, artifacts:read, artifacts:download | Can actually recover, and nothing else |
| CI key (machine) | backups:read, backups:write | Applies definitions from a pipeline |
| Monitoring key (machine) | backups:read, artifacts:read | Watches without touching |
The distinction between the first three is the one worth internalising: reading about a backup and reading its contents are different permissions, and most people only need the first.
Enforcement
Every workspace-scoped route declares the permission it requires. A request with a valid
credential and the wrong permission gets 403, not 404, so it is clear whether the problem
is identity or authorization.
Two gates are not permissions, because no permission could express them:
- Creating a workspace requires a human principal. A brand-new user has no workspace and therefore no workspace-scoped claim to check.
- Tenancy is a property of the credential rather than a permission, so there is no permission that would let one workspace read another.