Skip to content

Scope-Based Access Control

Overview

The scope system provides fine-grained authorization for API endpoints. Each endpoint declares the scope it requires via the @require_scopes decorator, and each role maps to a set of scopes. Enforcement checks the user's effective scopes at request time.

Key design decisions:

  • The Python registry defines all valid scopes and built-in role mappings
  • Custom role-scope assignments are stored in the role_scope database table
  • Effective scopes are the union of registry mappings and DB assignments
  • Scopes use the resource:action naming convention (e.g. user:read)
  • A fixed 5-action matrix (read | write | delete | manage | execute) across 17 resources = 85 scopes
  • Roles are organizational groupings that map to sets of scopes

Architecture

User authenticates (JWT)
  → AuthHandler resolves role keys from user_role table
  → AuthHandler resolves scopes from role keys via Python registry + DB
  → current_scope_keys_var set on context
  → ScopeAccessEnforcer reads @require_scopes metadata from handler
  → Checks current scopes satisfy the requirement
  → Request proceeds or 403

Registry Location

All scope and role-scope definitions live in:

src/mp_server/auth/scope_registry.py

How to Add a New Scope

All 85 scopes (17 resources x 5 actions) are pre-defined. To add a new resource, add 5 entries to the SCOPES dict (one per action):

python
SCOPES = {
  # ...existing scopes...
  "my_resource:read":    {"name": "Read my resource",    "description": "..."},
  "my_resource:write":   {"name": "Write my resource",   "description": "..."},
  "my_resource:delete":  {"name": "Delete my resource",  "description": "..."},
  "my_resource:manage":  {"name": "Manage my resource",  "description": "..."},
  "my_resource:execute": {"name": "Execute my resource", "description": "..."},
}

Then adjust the exclusion sets if the new resource should not be available to all roles.

How to Add a New Role

Option 1: Built-in role (Python registry)

Add the role key and its scope set to ROLE_SCOPES in scope_registry.py:

python
ROLE_SCOPES = {
  # ...existing roles...
  "my_role": ALL_SCOPE_KEYS - frozenset({"scope:to:exclude"}),
}

Option 2: Custom role (database)

  1. Create the role via POST /api/v1/role
  2. Assign scopes via POST /api/v1/role_scope_entry:
json
{
  "role_id": "<role-uuid>",
  "scope_key": "user:read"
}

DB-assigned scopes are merged with any built-in registry mappings for the same role.

Decorator Usage

python
from mp_server.auth.decorators import require_scopes

# Require a single scope
@require_scopes("user:read")

# Require any of multiple scopes (default mode="any")
@require_scopes("user:read", "user:write")

# Require all scopes
@require_scopes("user:read", "folder:read", mode="all")

Default Roles

RoleScopesSummary
adminAll 85Full access to everything.
provider72Everything except vault, webhooks, workflow authoring, and auth admin.
integration84Everything except auth admin. Designed for system-to-system API keys.
responder0No scopes. All responder actions (login, submit, consent) use open endpoints.

Endpoints open to all authenticated users (no scope required)

These endpoints use bearer auth but do not check scopes. Every role (including responder) can call them.

EndpointDescription
POST /auth/login/credentialsLog in with email + password
POST /auth/login/azure_adLog in via Azure AD
POST /auth/login/mfa_verifyComplete MFA challenge
POST /auth/refreshRefresh an access token
POST /auth/password_reset/requestRequest password reset email
POST /auth/password_reset/confirmConfirm password reset
POST /auth/mfa/setupSet up MFA (TOTP)
POST /auth/mfa/verifyVerify MFA code
PATCH /auth/mfa/enableEnable MFA for current user
POST /auth/device/loginComplete device login (uses registration token)
POST /auth/device/consentRecord device consent
GET /current_userGet current user profile, roles, and scopes
POST /user/change_passwordChange own password
GET /configGet public app config
GET /config/questionnaire/{id}Get effective config for a questionnaire
POST /questionnaire_submissionSubmit a questionnaire (authenticated)
POST /questionnaire_submission/anonymousSubmit anonymously (API key only)
POST /enrollment_consentGive consent to an enrollment
GET /enrollment_fetchFetch own enrollments with questionnaires

responder — 0 scopes

Responders are end-users (patients, participants) who log in, view assigned content, and submit responses. All responder actions use open endpoints (see table above) — no scopes are required. This means responders cannot access any administrative list/detail endpoints like GET /questionnaire, GET /folder, GET /device, etc.

provider — 72 scopes (all minus 13 excluded)

Providers are clinicians, researchers, or content managers. They can manage users, content, enrollments, devices, events, plugins, and workflows — but cannot access vault secrets, webhooks, author workflows, or administer auth.

Excluded scopes (13): vault:* (5), webhook:* (5), workflow:write, workflow:execute, auth:manage

ScopeEndpoints it unlocks
user:readGET /user, GET /user/{id}, GET /user_role, GET /user_role/{id}, GET /role, GET /role/{id}, GET /scope, GET /role_scope, GET /role_scope_entry, GET /role_scope_entry/{id}, GET /role/{id}/scope
user:writePOST /user, PATCH /user/{id}, POST /user_role, POST /role, PATCH /role/{id}, POST /role_scope_entry, DELETE /role_scope_entry/{id}, PUT /role/{id}/scope
user:deleteDELETE /user/{id}, DELETE /user_role/{id}, DELETE /role/{id}
questionnaire:readGET /questionnaire, GET /questionnaire/{id}, GET /questionnaire/{id}/revision, GET /questionnaire/{id}/revision/{id}, GET /questionnaire/{id}/revision/{id}/snapshot, GET /questionnaire/deleted
questionnaire:writePOST /questionnaire, PATCH /questionnaire/{id}, POST /questionnaire/{id}/restore
questionnaire:deleteDELETE /questionnaire/{id}
questionnaire:managePOST /questionnaire/{id}/publish
folder:readGET /folder, GET /folder/{id}, GET /folder/tree
folder:writePOST /folder, PATCH /folder/{id}
folder:deleteDELETE /folder/{id}
permission:readGET /folder_permission, GET /questionnaire_permission
permission:writePOST /folder_permission, PATCH /folder_permission/{id}, POST /questionnaire_permission, PATCH /questionnaire_permission/{id}
permission:deleteDELETE /folder_permission/{id}, DELETE /questionnaire_permission/{id}
questionnaire_submission:readGET /questionnaire_submission
enrollment:readGET /enrollment, GET /enrollment/{id}, GET /enrollment_user
enrollment:writePOST /enrollment, PATCH /enrollment/{id}, DELETE /enrollment/{id}
device:readGET /device, GET /device/{id}
device:deleteDELETE /device/{id}
config:readGET /config/folder/{id}
event_subscription:readGET /event_subscription, GET /event_subscription/{id}, GET /event_definition, GET /event_delivery_task
event_subscription:writePOST /event_subscription, PATCH /event_subscription/{id}
event_subscription:deleteDELETE /event_subscription/{id}
workflow:readGET /event_subscription/{id}/graph, GET /workflow_run, GET /workflow_run/{id}, GET /workflow_run/{id}/node
plugin:readGET /plugin_definition, GET /plugin_instance, GET /plugin_instance/{id}
plugin:writePOST /plugin_instance, PATCH /plugin_instance/{id}, DELETE /plugin_instance/{id}, POST /plugin_instance/{id}/invoke
analytics:readGET /analytics/dashboard/superset/embedded_uuid/embed

Provider also has all *:manage and *:execute scopes for resources not in the excluded list — these are reserved for future use.

integration — 84 scopes (all minus auth:manage)

Integration is for system-to-system API keys. It has full access to every resource including vault and webhooks, but cannot administer MFA, device registration, API keys, or refresh tokens.

Same as admin except: no auth:manage — so no access to POST /auth/device/register, GET/DELETE /refresh_token, or POST/GET/DELETE /api_key.

admin — all 85 scopes

Full access. In addition to everything provider and integration can do, admin also has:

ScopeEndpoints it unlocks
vault:readGET /vault_entry, GET /vault_entry/{id}
vault:writePOST /vault_entry, PATCH /vault_entry/{id}
vault:deleteDELETE /vault_entry/{id}
webhook:readGET /webhook, GET /webhook/{id}, GET /webhook/{id}/delivery, GET /webhook/{id}/delivery/{id}
webhook:writePOST /webhook, PATCH /webhook/{id}, POST /webhook/{id}/rotate_secret, POST /webhook/{id}/delivery/{id}/retry
webhook:deleteDELETE /webhook/{id}
workflow:writePUT /event_subscription/{id}/graph
workflow:executePOST /event_subscription/{id}/test, POST /workflow_run/{id}/context, POST /workflow/evaluate
auth:managePOST /auth/device/register, GET /refresh_token, GET /refresh_token/{id}, DELETE /refresh_token/{id}, POST /api_key, GET /api_key, GET /api_key/{id}, DELETE /api_key/{id}

Scope Catalogue (17 resources x 5 actions = 85 scopes)

Every resource has all five actions defined. Scopes marked active are currently enforced by at least one controller. Scopes marked reserved exist in the registry but are not yet used — they are available for future features or frontend gating.

user — Users, roles, and role assignments

ScopeStatusDescription
user:readactiveList and view users, roles, and role assignments.
user:writeactiveCreate and update users, roles, and assignments.
user:deleteactiveDelete users, roles, and role assignments.
user:managereservedAdministrative user management operations.
user:executereservedReserved for future user-related actions.

questionnaire — Questionnaires, revisions, and snapshots

ScopeStatusDescription
questionnaire:readactiveList and view questionnaires, revisions, and snapshots.
questionnaire:writeactiveCreate, update, and restore questionnaires.
questionnaire:deleteactiveDelete questionnaires.
questionnaire:manageactivePublish questionnaires.
questionnaire:executereservedReserved for future questionnaire actions.

folder — Folders and folder tree

ScopeStatusDescription
folder:readactiveList and view folders and folder tree.
folder:writeactiveCreate and update folders.
folder:deleteactiveDelete folders.
folder:managereservedReserved for future folder management.
folder:executereservedReserved for future folder-related actions.

permission — Folder and questionnaire permissions

ScopeStatusDescription
permission:readactiveList and view folder and questionnaire permissions.
permission:writeactiveCreate and update folder and questionnaire permissions.
permission:deleteactiveDelete folder and questionnaire permissions.
permission:managereservedReserved for future permission management.
permission:executereservedReserved for future permission actions.

questionnaire_submission — Questionnaire submissions

ScopeStatusDescription
questionnaire_submission:readactiveList and view questionnaire submissions.
questionnaire_submission:writereservedReserved for future submission writes.
questionnaire_submission:deletereservedReserved for future submission deletes.
questionnaire_submission:managereservedReserved for future submission management.
questionnaire_submission:executereservedReserved for future submission actions.

enrollment — Enrollments

ScopeStatusDescription
enrollment:readactiveList and view enrollments.
enrollment:writeactiveCreate and update enrollments.
enrollment:deleteactiveDelete enrollments.
enrollment:managereservedReserved for future enrollment management.
enrollment:executereservedReserved for future enrollment actions.

device — Devices

ScopeStatusDescription
device:readactiveList and view devices.
device:writeactiveCreate and update devices.
device:deleteactiveDelete devices.
device:managereservedReserved for future device management.
device:executereservedReserved for future device-related actions.

event_subscription — Event subscriptions

ScopeStatusDescription
event_subscription:readactiveList and view event subscriptions.
event_subscription:writeactiveCreate and update event subscriptions.
event_subscription:deleteactiveDelete event subscriptions.
event_subscription:managereservedReserved for future management.
event_subscription:executereservedReserved for future actions.

workflow — Workflow runs and graphs

ScopeStatusDescription
workflow:readactiveList and view workflow runs and graphs.
workflow:writeactiveCreate and update workflow graphs.
workflow:deleteactiveDelete workflow graphs.
workflow:managereservedReserved for future workflow management.
workflow:executeactiveTrigger workflow evaluations and context updates.

plugin — Plugin definitions and instances

ScopeStatusDescription
plugin:readactiveList and view plugin definitions and instances.
plugin:writeactiveCreate and update plugin instances.
plugin:deleteactiveDelete plugin instances.
plugin:managereservedReserved for future plugin management.
plugin:executereservedReserved for future plugin-related actions.

vault — Vault entries

ScopeStatusDescription
vault:readactiveList and view vault entries.
vault:writeactiveCreate and update vault entries.
vault:deleteactiveDelete vault entries.
vault:managereservedReserved for future vault management.
vault:executereservedReserved for future vault-related actions.

webhook — Webhooks and deliveries

ScopeStatusDescription
webhook:readactiveList and view webhooks and deliveries.
webhook:writeactiveCreate, update, and rotate secrets for webhooks.
webhook:deleteactiveDelete webhooks.
webhook:managereservedReserved for future webhook management.
webhook:executereservedReserved for future webhook-related actions.

analytics — Analytics data

ScopeStatusDescription
analytics:readactiveView analytics data.
analytics:writereservedReserved for future analytics write operations.
analytics:deletereservedReserved for future analytics delete operations.
analytics:managereservedReserved for future analytics management.
analytics:executereservedReserved for future analytics actions.

config — Configuration

ScopeStatusDescription
config:readactiveView configuration entries.
config:writereservedReserved for future configuration writes.
config:deletereservedReserved for future configuration deletes.
config:managereservedReserved for future configuration management.
config:executereservedReserved for future configuration actions.

auth — Authentication and security

ScopeStatusDescription
auth:readreservedReserved for future auth read operations.
auth:writereservedReserved for future auth write operations.
auth:deletereservedReserved for future auth delete operations.
auth:manageactiveMFA management, device registration, and API key administration.
auth:executereservedReserved for future auth-related actions.

questionnaire_builder — Frontend placeholder

ScopeStatusDescription
questionnaire_builder:readfrontendView questionnaire builder.
questionnaire_builder:writefrontendEdit in questionnaire builder.
questionnaire_builder:deletefrontendDelete in questionnaire builder.
questionnaire_builder:managefrontendManage questionnaire builder settings.
questionnaire_builder:executefrontendExecute questionnaire builder actions.

ai — Frontend placeholder

ScopeStatusDescription
ai:readfrontendView AI features.
ai:writefrontendConfigure AI features.
ai:deletefrontendDelete AI configurations.
ai:managefrontendManage AI settings.
ai:executefrontendExecute AI actions.

API Endpoints

GET /api/v1/scope

Returns all scopes defined in the Python registry.

Required scope: user:read

GET /api/v1/role_scope

Returns role-to-scope mappings, merging built-in registry mappings with any DB-persisted custom assignments. Each entry contains role_key and a sorted list of scopes.

Required scope: user:read

GET /api/v1/role_scope_entry

Paginated list of DB-persisted role-scope assignments. Supports filtering by role_id, scope_key, and search (partial match on scope_key).

Required scope: user:read

POST /api/v1/role_scope_entry

Assign a scope to a role. The scope_key must exist in the scope catalogue. Duplicate assignments are rejected with 409 Conflict.

Required scope: user:write

Request body:

json
{
  "role_id": "string",
  "scope_key": "string"
}

GET /api/v1/role_scope_entry/{id}

Retrieve a single role-scope assignment by ID.

Required scope: user:read

DELETE /api/v1/role_scope_entry/{id}

Soft-delete a role-scope assignment.

Required scope: user:write

PUT /api/v1/role/{id}/scope

Replace all scope assignments for a role with the provided list. Computes a diff against current DB state: adds missing scopes, soft-deletes removed scopes, and leaves unchanged scopes intact.

Required scope: user:write

Request body:

json
{
  "scope_keys": ["user:read", "user:write", "folder:read"]
}

Response:

json
{
  "role_id": "string",
  "scope_keys": ["folder:read", "user:read", "user:write"],
  "added": ["user:write"],
  "removed": ["device:read"],
  "unchanged": ["folder:read", "user:read"]
}

GET /api/v1/role/{id}/scope

Returns the scope keys currently assigned to a role in the DB.

Required scope: user:read

Response:

json
{
  "role_id": "string",
  "scope_keys": ["folder:read", "user:read"]
}

Database Schema

The role_scope table stores custom role-scope assignments:

ColumnTypeDescription
idstringPrimary key (UUID)
role_idstringFK to role.id
scope_keystringScope identifier (e.g. user:read)
created_atdatetimeRecord creation timestamp
updated_bystringUser who last modified
updated_atdatetimeLast modification timestamp
deleted_atdatetimeSoft-delete timestamp (null if active)

Built-in roles (admin, provider, integration, responder) get their scopes from the Python registry. DB assignments are additive — they extend (never reduce) a role's effective scope set.