Skip to content
CommonLayer
Documentation menu
On this page

ADR-0035: Approval decisions are human-only; service accounts never hold them

Status: Accepted (WP-70a, WP-70b; security review finding HIGH-2). Implemented in crates/commonlayer-core/src/authz/ (Permission::HUMAN_ONLY, Actor::require_human, Actor::require_human_session) and the deciding services.

Context

Several rules put a second person in the loop: a reward above the single-approval limit needs two approvals, an exception is decided by someone other than its requester, a payout run is approved by a second administrator. The independent security review of 2026-11-10 showed that one project admin could satisfy all of them alone: project admins can create project service accounts with the project_admin or maintainer role and give them full-scoped tokens, and the separation-of-duty checks compared only principal ids, so two service accounts counted as two other people.

Decision

  • A fixed set of approval-type permissions is human-only: platform.maintainers.approve, platform.eligibility.override, platform.payouts.approve, platform.ledger.adjust, project.security.exceptions.decide, project.contributions.review, project.rewards.approve. Actor::can never grants them to a service-account principal, whatever role it holds. Workload (CI) actors only ever hold project.artifacts.submit.
  • The deciding service methods additionally require a person:
    • Actor::require_human (a person, with a session or an API token) for exception decisions, not_affected confirmations, contribution reviews, task waivers, reward approvals and abuse-report resolution;
    • Actor::require_human_session (a person in an interactive session, never an API token) for payout-run approval and payout resolution, ledger adjustments, maintainer assignment approval and rejection, appeal resolution, eligibility overrides, and creating organization or project service accounts.
  • Refusals answer 403 authz.human_required or 403 auth.interactive_session_required.

Consequences

  • What an API token can never do is now two lists: the never-by-token permissions (platform.ledger.adjust, platform.payouts.approve, platform.users.manage, platform.settings.manage, credential changes) and the decisions above that need a session. A service account cannot approve anything at all.
  • Automation can still propose, request, submit and read; every approval is a person's audited act.
  • The rule is enforced in services, so REST and gRPC behave the same. Regression tests: authz_matrix::service_accounts_never_hold_human_only_permissions, confirmations::a_service_account_cannot_decide_an_exception, human_decisions::*, tokens::project_service_accounts, hardening::reward_and_payout_approvals_need_a_person.
  • Not done: treating a service account as its creator in separation-of-duty checks (the review's optional suggestion). It is unnecessary while service accounts cannot approve.