Skip to content

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:

  • admin
  • owner

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 <= now
  • ends_at is 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

Delegation matching rules

Delegation checks are time-aware and kind-aware.

Time rules

A delegation is considered active only when:

  • is_active = True
  • starts_at <= now
  • ends_at is null OR ends_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 403 when 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

  • IsAuthenticated
  • HasCurrentOrg
  • CanViewApproval OR CanDecideApproval

This layered approach ensures:

  1. user is authenticated
  2. org context is resolved
  3. 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