Feature Specification: Authorization of Callers

Created: 2026-09-23

Status: Implemented

Input: Reconstructed from the code of api/ at 0bf4ecd36, api/specification.md, pull requests #992, #1011, #1249 and #1903, issue #1894, and the design notes of the maintainers.

User Scenarios & Testing (mandatory)

User Story 1 - Let the front-end decide each call (Priority: P1)

api does not know users. For each call, it sends the name of the procedure (its operation identifier in the published contract), the scope of the call and the request headers to the authorization callback of the front-end. The callback answers whether the call is allowed, and optionally the subject, the issuer of the subject, a reason for a denial, and the deployments that the caller can see. api obeys the answer.

Why this priority: this keeps one identity authority, in the front-end that owns the accounts.

Independent Test: unit tests: a callback that denies gives an authorization error, and a callback that allows lets the call run.

Acceptance Scenarios:

  1. Given a callback that denies, When a client calls a protected procedure, Then api reports an authorization error and does nothing.
  2. Given a callback that allows, When a client calls a protected procedure, Then the call runs.
  3. Given a callback that allows with a set of deployments, When a client lists the deployments, with or without a filter, Then the list holds only deployments in that set.
  4. Given a callback that allows with a malformed set of deployments, When a client calls a protected procedure, Then api denies the call.
  5. Given a public procedure (the default schema and renames, the catalog and its icons, the application paths and the sections of a configuration in the request), When a client calls it, Then api does not call the callback.
  6. Given a callback that denies with a subject, an issuer and a reason, When a client calls a procedure that changes a deployment, Then api records an audit event with that actor, an authorization error as the result, and that reason.
  7. Given a callback that denies without a subject, When a client calls a procedure that changes a deployment many times in one interval, Then api records an audit event for the first call only, and the first such call after the interval records the count of the others as one event.

User Story 2 - Call from the browser with a bearer token (Priority: P1)

A browser calls api directly, on another origin than the front-end. It sends a short-lived bearer token that the front-end made. api does not read the token. It forwards it to the callback, which checks it.

Why this priority: the browser follows the progress of a deployment and orders the steps of a delete, so it talks to api directly.

Independent Test: in a VM test of a full deployment, a browser-like client with a token from the front-end submits a configuration.

Acceptance Scenarios:

  1. Given an allowed origin, When the browser calls api, Then the CORS headers allow the call.
  2. Given an origin that is not allowed, When the browser calls api, Then the browser blocks the response.
  3. Given an allowed origin, When api fails with an unhandled error, or the ingress makes the error response, Then the response still has the CORS headers, so the browser can read the error.

User Story 3 - Call from a trusted backend (Priority: P2)

A trusted backend service calls api for its users, without exposing api to them. It sends a static service token. On a match, api removes the token from the forwarded headers and marks the call as internal for the callback. The callback decides on behalf of whom the call runs.

Independent Test: unit tests: a call with the service token is internal for the callback, and the callback does not get the token.

Acceptance Scenarios:

  1. Given the correct service token, When the backend calls api, Then the callback gets an internal call and not the token.
  2. Given a wrong token, When a client sends it, Then api forwards the header as for any other call, and the callback decides.
  3. Given an internal call without a user on whose behalf it runs, When the callback gets it, Then the callback denies it.

Edge Cases

  • No callback is configured: api refuses to start, unless an explicit mode for development and tests allows all calls.
  • A destroy callback whose origin is not allowed: api refuses it, so api cannot be used to reach other hosts.

Requirements (mandatory)

Functional Requirements

  • FR-001: api MUST call the authorization callback for each protected procedure, and MUST report an authorization error when the answer does not allow the call.
  • FR-002: api MUST NOT check tokens of end users, keep sessions, or keep accounts.
  • FR-003: api MUST send the scope of the call (the deployment, the build, the deployments of a list, or the actor and its issuer of a list of audit events) to the callback, so the callback can apply its policy. api MUST take the scope as given, and MUST NOT parse a structure out of an identifier.
  • FR-004: api MUST compare the service token in constant time, and MUST NOT forward it to the callback. Only the callback MAY read the user on whose behalf an internal call runs; api MUST NOT read it.
  • FR-005: api MUST allow browser calls only from the configured origins. Each response to an allowed origin MUST have the CORS headers, also an unhandled error and an error of the ingress. The ingress MUST NOT add CORS headers when api sets them. CORS is not the defense against cross-site requests; the bearer token is.
  • FR-006: api MUST accept a destroy callback only when its origin is allowed, and MUST check the origin again before it calls the callback. The token of the callback MUST be one that the front-end made for that one deployment.
  • FR-007: Outside of development, api MUST refuse to start when no callback is configured. The development app and the tests MUST have an explicit way to allow all calls.
  • FR-008: api MUST name the procedure by its operation identifier in the published contract, so that the callback can keep a list of the procedures that it knows.
  • FR-009: When a callback token is configured, api MUST send it on each callback request, so that the callback can refuse requests that do not come from api.
  • FR-010: When an allowing answer has a set of deployments, a list MUST hold only the deployments in that set. api MUST deny a call whose answer has a malformed set. api MUST tell the callback that it limits lists to the set, so that the callback can refuse a list that an older api would not limit.
  • FR-011: The events service MUST ask the front-end to allow each connect, subscribe and publish.
  • FR-012: When a call also changes deployments that its request does not name, api MUST ask the callback again before the change, for the same procedure, with the deployment of the request and those deployments (deployment_ids) in the scope. For example, a domain claim takes the domain from the destroyed deployment that had it.

Key Entities

  • Authorization callback: an HTTP endpoint of the front-end that decides each call.
  • Service token: a static secret of a trusted backend.
  • Callback token: a static secret that api sends to the callback, to show that the request comes from api.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: No protected procedure runs without an allowing answer of the callback, when a callback is configured.
  • SC-002: api has no code that finds out who an end user is.

Assumptions

  • The front-end signs and checks its own tokens. api shares no secret with it, except the service token of a trusted backend and the callback token.

Known Deviations

None.