Approvals — Permissions¶
Overview¶
The approvals.permissions module contains object-level permission classes used by the approvals API.
These permissions answer a simple question:
- may this user view this approval?
- may this user decide this approval?
They are approval-specific permissions and should remain separate from:
- generic authentication
- org resolution
- service workflow logic
- serializer validation
This keeps authorization logic centralized and reusable.
Purpose¶
The approvals app has richer access rules than a simple “org member or admin” check.
A user may need access because they are:
- an org admin or owner
- directly assigned to an approval
- acting through an active delegation
These rules are used repeatedly across approval endpoints, so they belong in dedicated permission classes.
Permission classes¶
CanViewApproval¶
Determines whether a user may view a specific approval.
Access is allowed when¶
A user may view an approval if they are:
- an active org admin or owner
- a direct assignee on the approval
- a delegated actor for one of the approval’s assignees
Access is denied when¶
A user may not view an approval if:
- they are not an active member of the approval’s org
- they are not an admin/owner
- they are not directly assigned
- they are not currently delegated by one of the assignees
Message¶
"You are not allowed to view this approval."
CanDecideApproval¶
Determines whether a user may decide a specific approval.
Access is allowed when¶
A user may decide an approval if they are:
- an active org admin or owner
- a direct assignee on the approval
- a delegated actor for one of the approval’s assignees
Access is denied when¶
A user may not decide an approval if:
- they are not an active member of the approval’s org
- they are not an admin/owner
- they are not directly assigned
- they are not currently delegated by one of the assignees
Message¶
"You are not allowed to decide this approval."
Current permission model¶
Both permission classes currently use the same access logic.
That means the system treats:
- visibility rights
- decision rights
the same way for now.
This is acceptable for the current workflow, but it is important to note that these may diverge later.
For example, future requirements may allow:
- broader visibility
- narrower decision rights
- audit-only access
- observer roles
Keeping them as separate classes now makes future divergence easier.
Access rules in detail¶
Step 1: active org membership¶
The first requirement is always active membership in the approval’s organization.
If the user is not an active org member:
- permission is denied immediately
This ensures that approval access is always org-scoped.
Step 2: privileged org roles¶
The permission classes treat these org roles as privileged:
adminowner
If the user has one of these roles:
- permission is granted immediately
This provides org-level override access for approval administration.
Step 3: direct assignee access¶
If the user is directly assigned through ApprovalAssignee, permission is granted.
This includes:
- standard approvers
- forwarded assignees
because both are represented through assignment rows.
Step 4: delegated access¶
If the user is not directly assigned, the permission classes check whether the user is acting through an active delegation.
Delegated access is granted when there exists an active ApprovalDelegation such that:
- the delegation belongs to the same org
- the delegator is one of the approval assignees
- the delegate is the current user
- the delegation is active
starts_at <= nowends_atis null or still in the future- the delegation kind matches either:
- the approval kind
- blank string meaning all kinds
This allows delegated approvers to view and decide approvals on behalf of assigned users.
Permission flow¶
flowchart TD
A[Check approval permission] --> B{Active org membership?}
B -->|no| C[Deny]
B -->|yes| D{Admin or owner?}
D -->|yes| E[Allow]
D -->|no| F{Direct assignee?}
F -->|yes| E
F -->|no| G{Active matching delegation?}
G -->|yes| E
G -->|no| C
¶
flowchart TD
A[Check approval permission] --> B{Active org membership?}
B -->|no| C[Deny]
B -->|yes| D{Admin or owner?}
D -->|yes| E[Allow]
D -->|no| F{Direct assignee?}
F -->|yes| E
F -->|no| G{Active matching delegation?}
G -->|yes| E
G -->|no| CDelegation matching rules¶
Delegation checks are time-aware and kind-aware.
Time rules¶
A delegation is considered active only when:
is_active = Truestarts_at <= nowends_at is nullORends_at >= now
Kind rules¶
A delegation matches an approval when:
kind = ""(global delegation)- OR
kind == approval.kind
This allows both:
- global delegation
- approval-kind-specific delegation
Why permissions live outside views¶
Keeping approval permissions in a separate module gives several benefits:
- cleaner views
- reusable object-level authorization
- easier testing
- clearer ownership of access rules
- easier future expansion
Views should wire permissions.
They should not implement authorization logic inline.
Relationship to other layers¶
Permissions layer¶
Responsible for:
- determining whether the current user may access or act on a specific approval
Services layer¶
Responsible for:
- approval workflow behavior
- forwarding
- delegation lifecycle
- side effects
- notifications
Views layer¶
Responsible for:
- attaching permission classes to endpoints
- returning HTTP
403when permission fails
Models layer¶
Responsible for:
- storing assignments and delegations used by permission checks
What permissions do not do¶
These permission classes do not:
- decide whether an approval is pending
- decide whether forwarding is allowed
- decide whether an approval may be unforwarded
- create or disable delegations
- apply side effects
- send notifications
Those are workflow concerns and belong in services.
Permissions only answer:
- is this user allowed to access this approval in this way?
Usage in views¶
Typical usage:
CanViewApproval- attached to detail/retrieve endpoints
CanDecideApproval- attached to decision endpoints
Example pattern¶
IsAuthenticatedHasCurrentOrgCanViewApprovalORCanDecideApproval
This layered approach ensures:
- user is authenticated
- org context is resolved
- approval-specific object access is checked
Example scenarios¶
Scenario 1: org admin¶
A user is an active org admin.
Result:
- may view approval
- may decide approval
Scenario 2: direct assignee¶
A user is directly assigned to the approval.
Result:
- may view approval
- may decide approval
Scenario 3: delegated approver¶
A manager is assigned to the approval and has delegated approval responsibility to another user.
If the delegation is active and kind-matching:
- delegate may view approval
- delegate may decide approval
Scenario 4: unrelated org member¶
A user is in the org but is:
- not admin/owner
- not assigned
- not delegated
Result:
- may not view approval
- may not decide approval
Scenario 5: expired delegation¶
A user previously had delegation, but ends_at is now in the past.
Result:
- delegated access is denied
Design principles¶
Org-scoped first¶
Approval access is always scoped to active org membership.
Assignment-driven authorization¶
Approval access is based primarily on assignment state.
Delegation-aware access¶
Delegation is treated as a first-class authorization path.
Explicit privilege override¶
Admins and owners may bypass assignee checks.
Separate visibility and action permissions¶
Even though CanViewApproval and CanDecideApproval are currently equivalent, they are kept separate so future rules can diverge cleanly.
Best practices¶
- keep approval-specific permissions in
approvals.permissions - avoid embedding object permission logic inside views
- test both direct and delegated access paths
- test expired and inactive delegations explicitly
- keep permission checks focused on authorization only
- let services handle workflow validation separately
Future extensions¶
Possible future improvements include:
- separate observer/read-only permission rules
- dedicated forwarding permission class
- dedicated delegation-management permission class
- policy-helper extraction for shared approval authorization
- support for additional org roles beyond admin/owner
- multi-step approval access rules
Summary¶
The approvals.permissions module centralizes approval-specific authorization.
It currently supports access through:
- privileged org roles
- direct assignment
- active delegation
This keeps approval access control:
- explicit
- reusable
- delegation-aware
- separate from workflow logic