Approval workflow
What it does
Splits the apply step on every change request into a distinct approve click that a different admin must perform. Until approval, the change sits in awaiting_approval and cannot be applied to the customer's Entra tenant.
The workflow is workspace-wide: flip it on once in MSP settings and every tenant under that workspace inherits the two-admin gate.
When to use it
- Compliance frameworks (SOC 2, ISO 27001) that mandate dual control on production identity changes.
- Internal policy that no single admin should be able to push CA writes alone.
- Higher-trust customer tiers where you want to add an audit checkpoint without forcing a separate runbook.
If you're the only admin in the workspace, leaving require_approval off is the simpler choice - the four-eyes rule below means a single admin literally cannot apply anything.
Enabling it
- Sign in as Owner.
- Settings → Workspace → toggle Require approval on change requests.
- Save. The toggle takes effect immediately for every new change request. Existing in-flight changes keep the workflow they started under.
The toggle is the msp.require_approval boolean - the same column the server uses to decide which apply path to follow.
The new state machine
Without approval (require_approval = false):
draft → dry_run_complete → applied
With approval (require_approval = true):
draft → dry_run_complete → awaiting_approval → dry_run_complete → applied
↘ cancelled (if requester withdraws)
awaiting_approval is a parking state. The dry-run result is still valid (30-minute clock keeps running). When a second admin approves, the row drops back to dry_run_complete and the standard apply path takes over.
The four-eyes rule
The same admin that created the change cannot also approve it.
This is enforced server-side: the approve action rejects with cannot_self_approve if auth.uid() = change_request.created_by. Owners are not exempt - even an Owner who created the change must hand it to another admin.
Step-by-step: requester
- Tenant detail → Changes → Propose change, same as before.
- Fill in the payload, click Dry-run, review the warnings.
- Instead of Approve + apply, the button now reads Request approval.
- Click it. The change moves to
awaiting_approvaland a row appears under Changes awaiting approval on the workspace dashboard for every other admin to see.
Step-by-step: approver
- From the dashboard's Changes awaiting approval card (or a notification channel link), click into the change.
- Review the diff and warnings exactly as you would on a normal apply. The pre-change snapshot id is shown so you can verify the policy state being changed.
- If the diff matches expectations: click Approve and apply. Policytab atomically sets
approved_by = your-user-id,approved_at = now(), then runs the standard apply (pre-snapshot → Graph PATCH → post-snapshot). - If something looks wrong: click Reject (cancels the change with status
cancelled) and message the requester.
What the audit log records
Every approval emits a change_request.approved audit row with actor_user_id = approver, payload.created_by = original requester, payload.change_id, and the time delta from request → approval. The subsequent apply records its own change_request.applied row, so a fully audited change has three rows: proposed, approved, applied.
Troubleshooting
- Button is greyed out as "Cannot self-approve." You created this change. Ask another admin to take it.
- Approve button missing entirely. Your role is
readonly. Approval requiresadminorowner. awaiting_approvalrow says "Dry-run stale, re-run before approving." More than 30 minutes have passed since the dry-run. Open the change, click Re-run dry-run, then approve. The dry-run guarantee is part of why apply is safe - we never let an approver act on stale impact data.- I toggled the setting off but a change is still in
awaiting_approval. That's intentional: the setting decides which workflow new changes enter, but in-flight changes keep their original gate. Cancel and re-propose if you want to fast-track. - Approver clicks but apply fails with
graph_patch_failed. Same handling as a normal applied-then-failed change - see Troubleshooting. The approval timestamp is retained; only the apply attempt is recorded as failed.
Combining with scheduled changes
Approval and scheduling compose. An approver can approve a change that carries a scheduled_for timestamp - the approval is recorded immediately, and the cron picks the change up when the schedule fires. See Scheduled changes for the scheduler details.