ADR-0181: Studio enforces universal approver rules, backed by an access-change log
- Status: accepted
- Date: 2026-10-07
- Deciders: Mahdi Amirabdollahi
Context
An approval provider (ADR-0179) decides policy, but some rules make four eyes meaningless if any provider gets them wrong: a requester approving their own request, a token or agent approving, a stale session approving, or a requester creating or empowering a helper account while the request waits. Studio already has step-up within a five-minute window (ADR-0103), second factors (ADR-0143) and permission scopes (ADR-0172). Users are disabled, never deleted. Nothing records who changed whose access, and when, in a form a check can query.
Decision
- Deciding is session-only.
POST /api/v1/held-operations/{id}/decisionrefuses API tokens and agent origins, is CSRF-protected, and needs fresh authentication through the existing step-up flow. There is no MCP tool for deciding. - Studio applies these rules for every provider, before the provider's
checkVote:- The approver is not the requester (also a database CHECK), and does not share the requester's external identity or, case-insensitively, email.
- The approver holds the provider's declared approver permission now, at the operation's cluster scope.
- The approver's account was created before the request.
- The requester made no access change to anyone else after the request: no row in the access-change log with the requester as actor, another subject, and a later time. Any role, team, group-mapping or grant change counts, including indirect ones. This is the strict rule.
- The approver's session facts (
authenticatedAt,mfaVerifiedAt,mfaMethod) are passed tocheckVote, so a policy can require a second factor.
- A hold with no eligible approver is a deny ("No other user may approve this").
access_change_loglives in the security module. Columns(at, id, actor_id, subject_id);AccessChangeswrites one row in the caller's transaction on every access change; rows are kept 45 days, beyond any hold.- Every refusal is audited and appended to the request's timeline.
Consequences
- Self-approval, second accounts sharing an identity or email, approval by token or agent, stale sessions and freshly made helpers are closed for any provider, whatever its quality.
- The strict rule has a cost: a requester who does routine access work while a request waits blocks its approval and must request again. The user chose this over a narrower rule.
- Every access-changing service gains one write; a missed write would weaken rule 4, so the write sits in the shared helper the services already call.
- Unrelated identities belonging to the same person are not detectable; policies may restrict approvers further.
Alternatives considered
- Leave approver rules to the provider. Rejected: a provider bug would silently reopen self-approval.
- Only block changes that touch the approver permission. Rejected: indirect grants (a team, a group mapping, a default role) are too many paths to reason about; any change by the requester blocks.
- Read access history from the audit trail. Rejected: audit rows are written for people, with free-form parameters, and are not shaped for one indexed check.