Permission scopes
Every protected endpoint in the Translately API declares the scope(s) a caller must hold. Scopes are the atomic unit of authorization — API keys and PATs carry scope sets directly, user JWTs derive them from organization-role membership.
Introduced by: T108 (scope enum + @RequiresScope + filter), T109 (role → scope resolver).
Related: authorization architecture, error codes.
Naming
Each scope is a dotted, lowercase token: <domain>.<action> where <action> is read or write (with one exception: ai.suggest).
writeimpliesreadat resolver time:keys.writepasses akeys.readcheck.- Scopes are stable across minor versions. Add new ones, deprecate old ones, remove one minor later. Never rename.
- On the wire, scopes serialize as a space-separated string (the same grammar OAuth 2.0 uses):
- In JWTs — the
scopeclaim ("scope": "keys.read keys.write translations.write") plus a mirroredgroupsarray for@RolesAllowedinterop. - In API keys and PATs — the
api_keys.scopes/personal_access_tokens.scopesVARCHAR(512)column.
- In JWTs — the
Catalogue
The full 31-token catalogue is defined in io.translately.security.Scope. Tokens are grouped by domain:
Organization + membership
| Token | Purpose | Introduced |
|---|---|---|
org.read |
Read organization metadata | Phase 1 |
org.write |
Rename / update organization | Phase 1 |
members.read |
List organization members | Phase 1 |
members.write |
Invite / remove members, change roles | Phase 1 |
api-keys.read |
List API keys (prefix + metadata only) | Phase 1 |
api-keys.write |
Mint / revoke API keys | Phase 1 |
audit.read |
Read audit log entries | Phase 7 |
Project-wide
| Token | Purpose | Introduced |
|---|---|---|
projects.read |
List and read projects in an org | Phase 1 |
projects.write |
Create / archive projects | Phase 1 |
project-settings.write |
Rename / reconfigure / delete a project | Phase 1 |
Keys + translations (Phase 2)
| Token | Purpose |
|---|---|
keys.read |
List keys, read metadata |
keys.write |
Create / edit / delete keys, namespaces, tags |
translations.read |
Read translation values |
translations.write |
Author / edit translations |
Imports + exports (Phase 3)
| Token | Purpose |
|---|---|
imports.write |
Upload, preview, and run JSON imports |
exports.read |
Generate export bundles |
AI / MT + TM (Phase 4)
| Token | Purpose |
|---|---|
ai.suggest |
Invoke AI-suggest on a key or batch (BYOK) |
ai-config.write |
Configure provider, model, key, budget |
tm.read |
Read translation-memory matches |
glossaries.read |
Read glossary entries |
glossaries.write |
Create / edit glossary entries |
Screenshots (Phase 5)
| Token | Purpose |
|---|---|
screenshots.read |
Read screenshots pinned to keys |
screenshots.write |
Upload + pin screenshots |
Webhooks + CDN (Phase 6)
| Token | Purpose |
|---|---|
webhooks.read |
Read webhook configs + delivery log |
webhooks.write |
Create / edit / disable webhooks |
cdn.read |
Read CDN bundle config + URLs |
cdn.write |
Configure CDN content |
Tasks + branching (Phase 7)
| Token | Purpose |
|---|---|
tasks.read |
Read translation tasks |
tasks.write |
Create / assign / close tasks |
branches.read |
Read translation branches |
branches.write |
Create / merge / delete branches |
Role → scope mapping
The three built-in organization roles map to curated scope sets via ScopeResolver:
| Role | Scope set |
|---|---|
| OWNER | every scope in the catalogue — new scopes default to OWNER so we never forget to grant them |
| ADMIN | OWNER minus project-settings.write, ai-config.write, api-keys.write (retains audit.read) |
| MEMBER | every *.read scope plus keys.write, translations.write, imports.write, ai.suggest |
Invariant: OWNER ⊃ ADMIN ⊃ MEMBER. Enforced by OrgRoleScopesTest.
See docs/architecture/authorization.md for the rationale behind the ADMIN exclusion list.
How a scope is checked
- The authenticator (JWT / API key / PAT) resolves the caller’s full scope set into
SecurityScopes. ScopeAuthorizationFilterreads the@RequiresScope(...)annotation on the target resource method.- If
SecurityScopes ⊇ required, the request continues; otherwise the filter throwsInsufficientScopeException. InsufficientScopeExceptionMapperserialises that to a 403 with theINSUFFICIENT_SCOPEenvelope.
Multiple scopes on @RequiresScope(A, B) are an AND — the caller must hold every listed scope. If you need OR semantics, document it explicitly in the endpoint and express the alternative in code; don’t overload the annotation.
Minting API keys and PATs with scopes
When a user mints an API key (T110), they pick a subset of the scopes they currently hold (intersected with the org role). A MEMBER cannot mint an API key carrying api-keys.write — they don’t have it themselves. This intersection rule is enforced service-side, not UI-side; the UI hints but the server decides.
Forward compatibility
- Adding a scope. New scope lands in the enum and on OWNER by default. ADMIN and MEMBER pick up read scopes automatically (any
*.readjoins MEMBER) and opt-in for writes. - Deprecating a scope. Mark it deprecated in the enum +
CHANGELOGunder the release it lands. Keep it in responses for one minor version; remove under### Removedin the sunset release. - Unknown scopes in JWTs.
Scope.parsesilently drops tokens it doesn’t recognize — forward-compat for the case where an older server verifies a token minted by a newer one.
OpenAPI surface
Every endpoint in openapi.json carries an x-required-scopes extension listing the scopes its handler annotated with @RequiresScope. Generated SDK clients lift this into their types so IDE completion can surface the requirement at call sites.