Feature Specification: Authorization of calls to api
Created: 2026-09-23
Status: Implemented
Input: Reconstructed from the code of panel/ at 0bf4ecd36, api/specification.md, pull requests #992, #1011, #1036, #1249 and #1903, and the design notes of the maintainers.
User Scenarios & Testing (mandatory)
User Story 1 - api asks the panel who may do a call (Priority: P1)
api does not know users. The page gets a bearer token from the panel and calls api directly with it. For each call, api sends the procedure, the scope and the request headers to the authorization callback of the panel. 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 gives the identifiers of the deployments of the operator, and api lists only those. For an internal call, api has checked its own service token, and the panel takes the operator from the name that api gives on behalf of the user.
Why this priority: without this callback, no call to api is safe.
Independent Test: unit tests of the token and of the authorization callback.
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, When
apicalls the callback for a deployment of A, Then the panel allows the call and names A by the internal key of the account. For a deployment of operator B, the panel refuses, names A, and gives as the reason that the requester does not own the deployment. - Given a list of deployments without identifiers, from an
apithat says it applies a list of identifiers in the answer, Whenapicalls the callback, Then the panel allows the call and gives the identifiers of the deployments of the operator. - Given a token that is missing, expired or has a wrong signature, When
apicalls the callback, Then the panel refuses, with the reason that there is no requester. - Given an internal call on behalf of operator A, When the scope is a deployment of A, Then the panel allows the call. An internal call that names no user is refused, with the reason that there is no requester.
- Given a valid token of operator A and a list of audit events, When
apicalls the callback, Then the panel allows the call only when the actor of the scope is an identity of A: the OpenID Connect subject and issuer of A, or the internal key of A without an issuer. The panel also allows the call when A is a superuser. Else it refuses, with the reason that the requester is not the actor. - Given operator A that signed in with single sign-on, When the panel allows a call of A, Then the panel names A by the OpenID Connect subject of A and the issuer of the provider.
- Given a domain claim for a deployment of operator A, with the destroyed deployment that had the domain in the deployment identifiers, When
apicalls the callback, Then the panel allows the call only when A also operated the destroyed deployment. When the requester is a superuser, the panel allows the call for the deployments of each operator. The panel refuses a claim for a destroyed deployment. - Given a domain assignment for a deployment of operator A, When
apicalls the callback, Then the panel allows the call only when the requester is a superuser, the deployment exists and is not destroyed, and each of the deployment identifiers exists, also when it is destroyed. An owner of the deployment that is not a superuser cannot make the call.
User Story 2 - The browser follows the events of its own deployments (Priority: P1)
The browser connects to the event service (Centrifugo) with the session of the panel. Centrifugo asks the panel to accept the connection and each subscription. The panel accepts only the channels of the deployments of the operator, and refuses each publish from a client.
Why this priority: the events carry the state and the logs of builds, which are data of one tenant.
Independent Test: unit tests of the event proxy callbacks.
Acceptance Scenarios:
- Given a logged-in operator, When Centrifugo asks to accept a connection, Then the panel names the user by the internal key of the account. Without a session, the panel refuses.
- Given a channel of another operator, When Centrifugo asks to accept a subscription, Then the panel refuses.
- Given any client, When Centrifugo asks to accept a publish, Then the panel refuses.
User Story 3 - The panel 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 panel also gives the browser the list and the state of deployments through proxies.
Independent Test: unit tests of the client and of the proxies with an api that does not answer.
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 the browser calls a proxy, Then the proxy answers thatapiis unreachable, not with a server error.
Edge Cases
- The browser token expires during a long deploy: the page uses the token until shortly before its expiry time, and then gets a new token.
- A reconcile token is valid for one deployment only, and for a longer time than a browser token, because a destroy can take long.
- A token of one kind is not valid as a token of the other kind.
Requirements (mandatory)
Functional Requirements
- FR-001: The panel MUST make signed bearer tokens with a short lifetime, 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. Schema and rename lookups are the exception: they need only a valid operator. A list of audit events needs an operator that is the actor of the scope, or a superuser. The lists of usage and of domains also include the deployments that the operator destroyed. A domain claim needs an operator that owns the deployment of the scope, and that owns or destroyed each of the deployment identifiers. For the list of domains and a domain claim, a superuser has the rights of the operator of each deployment. The callback MUST match an identifier only in its exact form. For a list without identifiers, it MUST allow the call with the identifiers of the deployments of the operator, only for the lists of deployments, usage and domains, and only when
apisays that it applies such a list. It MUST refuse all other lists without identifiers. A domain assignment needs a superuser, for the deployments of each operator. - 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 with a short lifetime that names an operator.
- Reconcile token: a signed token that names a deployment. It cannot be used as an API token, and the reverse.
- Authorization request: the procedure, the scope, the request headers, whether the call is internal, and which extra fields of the answer
apiapplies. The answer says whether the call is allowed, names the operator, also in a denial of a known operator (the OpenID Connect subject and issuer of an account that signed in with single sign-on, else the internal key of the account and no issuer), and can give the deployment identifiers of a list. A denial has a reason without values of the request.
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 marks a call as internal, and removes the token from the headers that it sends.- Only
apipublishes events, through the administrative interface of Centrifugo. - The panel component generates the token with which
apicalls the authorization callback, and theapicomponent reads it from OpenBao.
Known Deviations
None.