Access reviews

Access reviews (access certification) are how you periodically prove that the people who have access still
need it. The server runs them as campaigns over a frozen snapshot. Code lives in
src/Domain/Governance/Reviews/.

Motivation

Grants accumulate. People change teams, projects end, contractors leave — but their access lingers. An
auditor will ask “who reviewed this access, and when?”. A campaign produces exactly that evidence: a dated,
signed certify/revoke decision per access item.

The lifecycle

stateDiagram-v2 [*] --> Created: POST campaigns Created --> Open: open (snapshot grants + signals) Open --> Open: certify / revoke items Open --> Closed: close Closed --> [*]
  1. Create & open a campaign

    curl -X POST https://iam.example.com/api/iam/v1/access-reviews/campaigns \
      -H "Authorization: Bearer $ADMIN_TOKEN" -d '{"name":"Q3 warehouse review","scope":{...}}'
    curl -X POST https://iam.example.com/api/iam/v1/access-reviews/campaigns/{campaign}/open \
      -H "Authorization: Bearer $ADMIN_TOKEN"
    

    CampaignEngine generates items (subject × access) and freezes a snapshot of grants and signals.

  2. Review items with context. ReviewSignals attaches risk signals — unused grants, anomalies — to
    each item so reviewers decide with evidence:

    curl https://iam.example.com/api/iam/v1/access-reviews/campaigns/{campaign}/items \
      -H "Authorization: Bearer $ADMIN_TOKEN"
    
  3. Certify or revoke each item (both audited):

    curl -X POST https://iam.example.com/api/iam/v1/access-reviews/items/{item}/certify -H "Authorization: Bearer $ADMIN_TOKEN"
    curl -X POST https://iam.example.com/api/iam/v1/access-reviews/items/{item}/revoke  -H "Authorization: Bearer $ADMIN_TOKEN"
    
  4. Close the campaign:

    curl -X POST https://iam.example.com/api/iam/v1/access-reviews/campaigns/{campaign}/close -H "Authorization: Bearer $ADMIN_TOKEN"
    

From the CLI: php artisan iam:reviews:open --campaign=..., iam:reviews:remind --campaign=...,
iam:reviews:close --campaign=....

Snapshot, not live data

A campaign evaluates a frozen snapshot

Removing a role from the catalog after a campaign opens must not retroactively change its outcome — and
must never leave a permanent orphan grant. The campaign decides against the grants and signals as they were
when it opened.

Feature gating

Access reviews are a governance feature gated per layer / app / role / user via NativeFeatureScope. The
default is on (iam-governance.phpfeatures.access_review, permission
iam:access_review.manage). See Configuration.

State transitions are locked

Opening, closing and item decisions are read-then-write transitions. The server runs them under
DB::transaction + lockForUpdate + re-check, so two concurrent closes or a late catalog change can’t
produce orphan grants or double certifications. This is a hard TOCTOU invariant of the package.

Next