Skip to content

Scopes

Every endpoint behind an OAuth2-protected route requires one or more scopes. The platform's scope catalogue uses a flat <resource>:<action> shape — cases:read, patients:write, images:read, derm_review:write, and so on. The full live catalogue is rendered on the admin Integration tab for any given (organisation, product).

Three forms a granted scope can take

When an operator provisions your OAuth2 client they hand-pick the scopes you're allowed to ask for. The grant can be expressed in three ways:

Form Example What it covers
Exact cases:read Just that one scope
Prefix wildcard cases:* Every cases:<action> scope, present and future
Global wildcard * Every scope in the catalogue

The matching is dynamic — when the platform adds a new cases:archive scope, an integration granted cases:* picks it up automatically. No re-issuing of credentials needed.

The match logic only applies in the granted slot. Required scopes (the ones declared on platform endpoints) are always concrete: a wildcard in your @RequireScopes(...)-equivalent decorator (if you were building one) would be a footgun.

When to ask for a wildcard grant

Ask for When
Exact list (cases:read, cases:write, images:read) You're doing the "request only what you need" thing carefully and want operators to ratify every new permission
Prefix (cases:*, images:*) Your integration is the system of record for that resource — adding new actions should Just Work
Global (*) Internal system clients only. Don't request this for third-party integrations

There is no functional difference between being granted ['cases:read', 'cases:write'] and ['cases:*'] today — the difference is whether the next quarterly cases:<new-action> scope flows to you automatically or needs an operator action.

How scopes appear in tokens

The access token's scopes claim carries the grant verbatim — so exact lists stay exact, wildcards stay as wildcards. Resource servers expand wildcards at request time via the shared scopeMatches(granted, required) helper from @sa-platform/common.

For tokens minted via the token-exchange grant (clinician delegation), the picture is different: the issued delegated token carries the concrete intersection of (consumer's granted scopes, user's role permissions). That way downstream audit logs attribute the action to a precise capability, not a wildcard category. A consumer holding cases:* exchanging on behalf of a clinician whose role grants cases:read and cases:write gets a delegated token with scopes: ['cases:read', 'cases:write'].

Requesting a subset in a token request

The scope= parameter on POST /v1/auth/oauth/token is a downscope — it narrows the scopes baked into the issued token, never widens them. Requested scopes must be concrete; wildcards in scope= return invalid_scope. Example:

POST https://platform.example.com/v1/auth/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<id>
&client_secret=<secret>
&scope=cases:read patients:read

If you ask for cases:read and your client has cases:*, you get a token carrying just cases:read — useful for short-lived tokens issued to less-trusted callers within your stack.

Baseline catalogue

The platform seeds ~25 baseline permissions on every user-management boot (idempotent via upsert-by-permission-string). The current set:

Service Permissions
platform admin:read, admin:write, platform:admin
clinical-api cases:{read,write}, patients:{read,write}, images:{read,write}, histology:{read,write}, medications:{read,write}, events:read, webhooks:{read,write}, derm_review:{read,write}
orchestrator orchestrator:{read,write,publish,intervene}, question_sets:{read,write,publish}

Operators can add more by hitting POST /v1/user-management/permissions/register on user-management — the baseline only exists so the admin console's Create-Role page has something to populate the multi-select with on a fresh deployment.