Feature Specification: Authorization of Calls to api
Created: 2026-09-23
Status: Implemented
Input: Reconstructed from the code at 0bf4ecd36 (panel/src/panel/tokens.py, views.py (ApiToken, ReconcileToken, AuthzCallback, CentrifugoConnect, CentrifugoSubscribe, CentrifugoPublish, DeploymentListProxy, DeploymentStateProxy), api_client.py, nix/openapi-client.nix, static/centrifuge_session.js), api/specification.md (sections "Authorization" and "Events"), pull requests #992, #1011, #1036, #1249 and #1903, and the design notes of the maintainers.
User Scenarios & Testing (mandatory)
User Story 1 - The browser calls api as the operator (Priority: P1)
The page asks the panel for a bearer token (POST /api/token/), and calls
api directly with it. api sends the procedure, the scope and the request
headers to the authorization callback of the panel (POST /api/authz/). The
panel finds the operator from the token, and allows the call only when the
operator owns the scope. For a list without identifiers, the panel allows the
call and gives the identifiers of the deployments of the operator, and api
lists only those.
Why this priority: api does not know users. Without this callback, no
call to api is safe.
Independent Test: run panel-python-tests (test_deployment_async.py,
test_api_client.py).
Acceptance Scenarios:
- Given a logged-in operator, When the page asks for a token, Then the panel gives a signed token and its expiry time. An anonymous request gets no token.
- Given a valid token of operator A and a scope with the identifier of a deployment of A, When
apicalls the callback, Then the panel answers{allow: true, sub: "<pk of A>"}. - Given a valid token of operator A and a deployment identifier of operator B, When
apicalls the callback, Then the panel answers{allow: false}. - Given a list of deployments with one or more
deployment_idsin the scope, Whenapicalls the callback, Then the panel allows the call only when each identifier is of a deployment of the operator. - Given a valid token of operator A and a list of deployments without
deployment_idsin the scope and withaccepts: ["deployment_ids"], Whenapicalls the callback, Then the panel answers{allow: true, sub: "<pk of A>", deployment_ids: [<the ids of the deployments of A>]}. - Given a schema or rename lookup, When
apicalls the callback with a valid token, Then the panel allows the call. - Given a token that is missing, expired or has a wrong signature, When
apicalls the callback, Then the panel answers{allow: false}.
User Story 2 - api calls on behalf of the operator (Priority: P2)
For an internal call, api has checked its own service token, and sends
internal: true. The panel then takes the operator from the
X-On-Behalf-Of header, and applies the same rules.
Independent Test: run panel-python-tests (test_deployment_async.py).
Acceptance Scenarios:
- Given
internal: trueandX-On-Behalf-Of: A, When the scope is a deployment of A, Then the panel allows the call. - Given
internal: truewithoutX-On-Behalf-Of, Whenapicalls the callback, Then the panel answers{allow: false}.
User Story 3 - The browser follows the events of its own deployments (Priority: P1)
The browser connects to Centrifugo with the session cookie of the panel.
Centrifugo asks the panel to accept the connection and each subscription. The
panel accepts only the channels fediversity.deployments.<pk> of the
deployments of the operator, and refuses each publish from a client.
Independent Test: run panel-python-tests (test_centrifugo_proxy.py).
Acceptance Scenarios:
- Given a logged-in operator, When Centrifugo calls
connect, Then the panel gives the primary key of the user. Without a session, the panel refuses. - Given a channel of another operator, When Centrifugo calls
subscribe, Then the panel refuses. - Given any client, When Centrifugo calls
publish, Then the panel refuses.
User Story 4 - The server calls api for the operator (Priority: P2)
For the pages that the panel renders, the panel calls api with the client
that is generated from the OpenAPI document of api, and with a token of the
operator. The proxies deployments/api/ and deployments/<pk>/state/ give
the browser the list and the state as JSON.
Independent Test: run panel-python-tests (test_api_client.py,
test_api_down.py).
Acceptance Scenarios:
- Given a page that shows state, When the panel calls
api, Then the call carries a token of the operator, andapiauthorizes it through the callback. - Given that
apidoes not answer, When a proxy is called, Then the proxy answers with an "unreachable" error, not a server error.
Edge Cases
- The browser token expires during a long deploy: the page keeps the token until shortly before its expiry time, and then asks for a new token.
- A reconcile token is valid for one deployment only, and for a longer time (
RECONCILE_TOKEN_TTL), because a destroy can take long. - A token of one kind is not valid as a token of the other kind (separate salts).
Requirements (mandatory)
Functional Requirements
- FR-001: The panel MUST mint signed bearer tokens with a short lifetime (
API_TOKEN_TTL) only for a logged-in operator. - FR-002: The authorization callback MUST allow a call only for an operator that owns the deployment identifier, or each of the deployment identifiers, in the scope, except the schema and rename lookups, which need only a valid operator. It MUST match an identifier only in the form
str(pk). For a list without identifiers, it MUST allow the call with the identifiers of the deployments of the operator asdeployment_ids, only fordeployment_list, and only whenacceptsof the request hasdeployment_ids. Else it MUST refuse a list without identifiers. - FR-003: The authorization callback MUST refuse a call that has no operator.
- FR-004: The event proxy callbacks MUST limit each operator to the channels of their own deployments, and MUST refuse publishes from clients.
- FR-005: The panel MUST call
apionly through the client generated from the OpenAPI document ofapi, or through the documented HTTP interface. - FR-006: The authorization callback MUST allow only the procedures of a list that the panel knows, and MUST refuse all other procedures. A test MUST compare the list with the OpenAPI document of
api. - FR-007: The authorization callback MUST accept calls only from
api.
Key Entities
- API token: a signed token that names an operator, salt
panel.api-token. - Reconcile token: a signed token that names a deployment, salt
panel.reconcile-token. - Authorization request:
{procedure, scope, headers, internal?, accepts?}fromapi. The answer is{allow, sub?, deployment_ids?}.acceptslists the fields of the answer, besidesallowandsub, thatapiapplies.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: No call to
apiwith the token of one operator reads or changes a deployment of another operator. - SC-002: No client can publish to an event channel.
Assumptions
apichecks its service token before it sendsinternal: true, and removes the token from the headers.- Only
apipublishes events, through the admin API of Centrifugo. - The panel component generates the callback token, and the api component reads it from OpenBao.
Known Deviations
None.