Permission guards
Quorum enforces a layered permission model on every approval action. When a user attempts to approve or reject a request, each of these checks is evaluated in order:
| Guard | Scope | Description |
|---|---|---|
| Self-approval prevention | Built-in | The maker of a request cannot approve their own request. Always enforced. |
| Eligible reviewers | Per-request | An optional allowlist of user IDs set when creating the request. If provided, only those users can act. |
| Allowed checker roles | Per-stage | Each approval stage can restrict which roles are permitted. The checker must hold at least one matching role. |
| Allowed permissions | Per-stage | Each approval stage can restrict which permissions are required. The checker must hold at least one matching permission. |
| Authorization mode | Per-stage | When both allowed_checker_roles and allowed_permissions are set, controls whether either suffices ("any") or both are required ("all"). |
| Reviewer scope | Per-stage | scope rules require the checker’s identity attribute to equal the maker’s attribute of the same name, or a value at a payload_path. See Reviewer scoping. |
| Dynamic authorization URL | Per-policy | An external HTTP endpoint called before each approval. The endpoint receives the full request context and returns {"allowed": true/false}. Use this for custom business logic your domain requires. |
| Duplicate vote prevention | Per-stage | A checker can only vote once per stage. They may vote on different stages of the same request. |
| Request fingerprinting | Per-policy | Configurable identity_fields extracted from the payload and hashed. Prevents duplicate pending requests for the same resource. |
| Bypass rules | Per-policy | bypass_rules describe requests that need no review. A matching request is approved on creation and audited as bypassed. See Bypass rules. |
Approve or reject arrives
- Self-approval checkalways403 when the maker cannot approve their own request
- Eligible reviewersif configured403 when the checker is not in the request’s allowlist
- Allowed checker rolesif set on the stage403 when the checker lacks a required role
- Allowed permissionsif set on the stage403 when the checker lacks a required permission
- Reviewer scopeif set on the stage403 when the checker is outside the stage’s scope
- Dynamic authorization URLif configured403 when your endpoint returned allowed: false
- One vote per stagealways409 when the checker already voted on this stage
Vote recorded
A rejected guard returns 403. The viewer_can_act field on a request reflects the same checks for the current caller, which is how the widgets know whether to render the approve and reject buttons.
Dynamic authorization check
Section titled “Dynamic authorization check”When a policy has a dynamic_authorization_url, Quorum POSTs the full request context to your endpoint before allowing the approval: request ID, type, checker and maker IDs, their identity attributes (maker_attributes and checker_attributes), and the payload. Your endpoint responds with {"allowed": true} or {"allowed": false, "reason": "..."}.
This lets you implement arbitrary business rules without modifying Quorum: department separation, conflict-of-interest checks, rate limiting, or a lookup in a system Quorum knows nothing about.
Quorum is built by Olawale Lawal.