[Platform API] Authorization Design: Scope-Based Access Control for Platform API #2045
Replies: 3 comments 3 replies
-
|
Shall we define some conventions, something like this? Rule 1 — Lock the shape
Rule 2 — CRUD verbs are a closed set
Rule 3 — Custom verbs only for genuine non-CRUD
Rule 4 — Wildcards:
|
| Wildcard | Covers |
|---|---|
api-platform:* |
Every action on root-level resources (gateway:create, rest_api:read, …). Own-level like every :* — not sub-resources such as gateway:token:* or application:api_key:*. |
api-platform:<resource>:* |
All actions directly on the resource. Not its sub-resources. |
api-platform:<resource>:<sub>:* |
All actions directly on that sub-resource. |
Rule 5 — URLs first, scopes follow
- Design the REST URL first; derive the scope from it. The convention constrains scope shape, not URL design.
- The scope reflects the subject being operated on, not a literal echo of the URL path:
| URL | Scope | Subject |
|---|---|---|
POST /rest-apis/{id}/devportals/publish |
rest_api:publish |
the REST API |
POST /gateways/{id}/tokens |
gateway:token:create |
a token (owned by gateway) |
Beta Was this translation helpful? Give feedback.
-
|
@Thushani-Jayasekera @malinthaprasan, I reviewed the Platform API OpenAPI specification and noticed some resource level inconsistencies. Below are my suggestions. 1. Conflicting GETs
Convention rule: never add a sibling GET literal next to 2. Action namespaces
3. Publish / unpublish →
|
| Current | Proposed |
|---|---|
POST /rest-apis/{apiId}/devportals/publish |
POST /rest-apis/{apiId}/publications (body: devportalId) |
POST /rest-apis/{apiId}/devportals/unpublish |
DELETE /rest-apis/{apiId}/publications/{devportalId} |
GET /rest-apis/{apiId}/publications |
unchanged |
4. Deployment actions → scope to the instance
| Current | Proposed |
|---|---|
POST /…/deployments/undeploy |
POST /…/deployments/{deploymentId}/undeploy |
POST /…/deployments/restore |
POST /…/deployments/{deploymentId}/restore |
{id}-scoped POST verbs are fine (no same-method conflict). Clarify the model: undeploy = remove from gateway, keep record; delete = remove record. (Alternative: PATCH /{id} on a status field.) Applies to all six deployment families: rest / llm-provider / llm-proxy / mcp / websub / webbroker.
5. Remove /me
| Current | Proposed |
|---|---|
GET /me/api-keys |
GET /api-keys (caller's keys; owner implicit from the JWT) |
6. Minor consistency
| Current | Proposed | Note |
|---|---|---|
…/{customPolicyUuid}/version/{version} |
…/{customPolicyUuid}/versions/{version} |
Plural sub-collection |
GET /llm-providers/{id}/llm-proxies |
GET /llm-proxies?providerId={id} |
Filter over nesting (optional) |
POST/GET /rest-apis/{apiId}/gateways |
review vs deployments |
Possible overlap — a deployment already implies a gateway |
Beta Was this translation helpful? Give feedback.
-
Platform API - Auth related configsMode 1: Local JWT (quickstart default)Mode 2: External IDP (production)Claim mappings (only needed if your IDP uses non-standard names)Validation mode: "scope" (default) or "role" |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Overview
This document describes the authorization model used by the Platform API. The goal is to give future developers a clear picture of how access control works end-to-end, from how scopes are defined, to how they are enforced at runtime, and how role-based tokens from external IDPs are handled.
The core design principles are:
openapi.yamlusing thex-required-scopesextension. The enforcement middleware reads this at startup, there is no per-route permission annotation in handler code.:managescope that acts as a full superset, covering read and all write operations for that resource.Scope Reference
Endpoint Scope Reference
Required scopes per endpoint (OR-evaluated — any one scope from the list is sufficient).
Projects
POST /projectsapi-platform:project:create,api-platform:project:manageGET /projectsapi-platform:project:read,api-platform:project:manageGET /projects/{projectId}api-platform:project:read,api-platform:project:managePUT /projects/{projectId}api-platform:project:update,api-platform:project:manageDELETE /projects/{projectId}api-platform:project:delete,api-platform:project:manageREST APIs
POST /rest-apis/import-openapiapi-platform:rest_api:import,api-platform:rest_api:managePOST /rest-apis/validate-openapiapi-platform:rest_api:read,api-platform:rest_api:manageGET /rest-apisapi-platform:rest_api:read,api-platform:rest_api:managePOST /rest-apisapi-platform:rest_api:create,api-platform:rest_api:manageGET /rest-apis/{apiId}api-platform:rest_api:read,api-platform:rest_api:managePUT /rest-apis/{apiId}api-platform:rest_api:update,api-platform:rest_api:manageDELETE /rest-apis/{apiId}api-platform:rest_api:delete,api-platform:rest_api:manageGET /rest-apis/{apiId}/gatewaysapi-platform:rest_api:gateway:read,api-platform:rest_api:gateway:manage,api-platform:rest_api:manage,api-platform:gateway:read,api-platform:gateway:managePOST /rest-apis/{apiId}/gatewaysapi-platform:rest_api:gateway:create,api-platform:rest_api:gateway:manage,api-platform:rest_api:manageGET /rest-apis/{apiId}/publicationsapi-platform:rest_api:publication:read,api-platform:rest_api:managePOST /rest-apis/{apiId}/publicationsapi-platform:rest_api:publication:create,api-platform:rest_api:manageDELETE /rest-apis/{apiId}/publications/{devportalId}api-platform:rest_api:publication:delete,api-platform:rest_api:managePOST /rest-apis/{apiId}/deploymentsapi-platform:rest_api:deployment:create,api-platform:rest_api:deployment:manage,api-platform:rest_api:manageGET /rest-apis/{apiId}/deploymentsapi-platform:rest_api:deployment:read,api-platform:rest_api:deployment:manage,api-platform:rest_api:manageGET /rest-apis/{apiId}/deployments/{deploymentId}api-platform:rest_api:deployment:read,api-platform:rest_api:deployment:manage,api-platform:rest_api:manageDELETE /rest-apis/{apiId}/deployments/{deploymentId}api-platform:rest_api:deployment:delete,api-platform:rest_api:deployment:manage,api-platform:rest_api:managePOST /rest-apis/{apiId}/deployments/{deploymentId}/undeployapi-platform:rest_api:deployment:undeploy,api-platform:rest_api:deployment:manage,api-platform:rest_api:managePOST /rest-apis/{apiId}/deployments/{deploymentId}/restoreapi-platform:rest_api:deployment:restore,api-platform:rest_api:deployment:manage,api-platform:rest_api:manageREST API Keys
POST /rest-apis/{apiId}/api-keysapi-platform:rest_api:api_key:create,api-platform:rest_api:api_key:managePUT /rest-apis/{apiId}/api-keys/{keyName}api-platform:rest_api:api_key:update,api-platform:rest_api:api_key:manageDELETE /rest-apis/{apiId}/api-keys/{keyName}api-platform:rest_api:api_key:delete,api-platform:rest_api:api_key:manageGET /me/api-keys(replaced with new endpoint)api-platform:rest_api:api_key:read,api-platform:rest_api:api_key:manageGit
POST /git/repo/fetch-branchesapi-platform:git:readPOST /git/repo/branch/fetch-contentapi-platform:git:readPlatform Roles (Not supported in M1)
When an IDP issues role names instead of fine-grained scopes, the Platform API maps those role names to one of five built-in platform roles, each with a fixed permission set.
admindeveloperpublisheroperatorviewerIDP role names (e.g.
PLATFORM_ADMIN,realm_developer) are mapped to platform roles via theIDP_ROLE_MAPPINGSconfiguration. The mapping happens during authentication so the scope enforcer always works with platform roles, not IDP-specific names.Adding a New Resource
Every new resource follows the same three-step pattern.
1. Define the scopes using snake_case with colons as separators. Every resource gets a standard set:
Add action-specific scopes alongside these where needed (
resource:publish,resource:import, etc.). For sub-resources, nest the name:resource:sub_resource:action.Please follow the rules in : #2045 (comment) (Rule 4 is not implemented in M1, kept for future reference)
2. Declare in
openapi.yaml— Required scopes are declared on each operation using the standard OpenAPI security field under the OAuth2Security scheme:Authorization Flow
Example —
IDP_VALIDATION_MODE=scopeConfig:
IDP_VALIDATION_MODE=scopeToken scope claim:
api-platform:rest_api:manage api-platform:project:readRequest:
POST /api/v1/rest-apisRequired scopes (from OpenAPI):
[api-platform:rest_api:create, api-platform:rest_api:manage]The enforcer reads the scope claim, finds
api-platform:rest_api:manage→ allowed. The manage scope satisfies the create requirement because it is listed alongsiderest_api:createinx-required-scopes. Roles are not looked at.Example —
IDP_VALIDATION_MODE=role(Not supported in M1)Config:
IDP_VALIDATION_MODE=role,IDP_ROLES_CLAIM_PATH=realm_access.roles,IDP_ROLE_MAPPINGS=PLATFORM_DEV=developerToken:
realm_access.roles = ["PLATFORM_DEV"]The authentication middleware maps
PLATFORM_DEV→developerand stores it asplatform_roles. The scope claim is not read at all.The scope enforcer expands the
developerrole to its full permission set, converts each permission to its scope string, and checks against the required scopes. Developer includesrest_api:create→ allowed. The scope claim is not consulted.OpenAPI as the Source of Truth
RRequired scopes are declared on each operation using the standard OpenAPI security field under the OAuth2Security scheme:
The list is OR-evaluated: having any one scope from the list is sufficient. Write operations always list both the specific scope and the manage scope so that both fine-grained tokens and manage-scope tokens are accepted. Scopes follow the ap:: naming convention.
At server startup, openapi.yaml is parsed, path parameters are converted from {param} to Gin's :param syntax, the server base path
(/api/v1) is prepended from the first servers[].url entry, and an in-memory lookup table is built. Handler code contains no permission
annotations. To change authorization requirements for a route, edit the security field in the OpenAPI spec only.
Operations that require no authentication declare security: [] to opt out (e.g. POST /organizations).
Beta Was this translation helpful? Give feedback.
All reactions