Authorization frameworks and patterns

As authorization rules grow complex, consider dedicated frameworks: Cerbos (open-source policy engine), Auth0 (managed service), AWS IAM (if on AWS). They centralize rules, make them testable, and scale to thousands of permissions. Without them, authorization logic scattered across your code becomes unmaintainable.

Advanced

5 min read

The problem: authorization sprawl

As apps grow, authorization logic spreads everywhere:

# In view 1: can delete a post?
if user.role == "admin" or (user.role == "editor" and user.id == post.owner_id):
    delete_post(post_id)
 
# In view 2: can comment on a post?
if user.role != "banned" and post.allow_comments:
    add_comment(post_id, user.id, text)
 
# In view 3: can see post?
if user.role in ["admin", "editor", "viewer"] or post.is_public:
    return post
 
# In view 4: can see analytics?
if user.role == "admin" or (user.organization_id == post.organization_id and user.role in ["admin", "editor"]):
    return analytics

Problems:

  • Rules are duplicated across views.
  • Hard to test: authorization logic is mixed with business logic.
  • Hard to change: update a rule and find 5 places to change it.
  • Easy to miss edge cases: "oh, I forgot to check the organization_id in this query."

Authorization frameworks solve this

An authorization framework centralizes rules in one place:

# authorization.yaml
rules:
  delete_post:
    - user.role == "admin"
    - user.role == "editor" AND user.id == resource.owner_id
  
  comment_on_post:
    - user.role != "banned" AND resource.allow_comments
  
  view_post:
    - user.role IN ["admin", "editor", "viewer"]
    - resource.is_public
  
  view_analytics:
    - user.role == "admin"
    - user.organization_id == resource.organization_id AND user.role == "editor"

Then your code calls the framework:

@app.delete('/posts/<post_id>')
def delete_post(post_id):
    post = db.get_post(post_id)
    
    # Centralized check
    if not authz.allow(request.user, 'delete_post', post):
        abort(403)
    
    db.delete_post(post_id)
    return 200

Benefits:

  • Rules are in one place.
  • Easy to test: separate from business logic.
  • Easy to change: update once, affects all views.
  • Auditable: view exactly what permissions are allowed.

Cerbos: open-source policy engine

Cerbos is a dedicated authorization service. You define policies in Cerbos, and your app asks Cerbos for decisions.

Install and run:

docker run -d -p 3592:3592 ghcr.io/cerbos/cerbos:latest

Define a policy:

# posts.yaml
apiVersion: api.cerbos.dev/v1
metadata:
  resource: "post"
rules:
  - actions: ['delete']
    effect: EFFECT_ALLOW
    principals:
      - admin
    condition:
        match:
          expr: "true"  # admins can delete any post
  
  - actions: ['delete']
    effect: EFFECT_ALLOW
    principals:
      - editor
    condition:
        match:
          expr: "principal.id == resource.owner_id"  # editors can delete own posts

Query from your app:

import grpc
from cerbos import cerbos_service_pb2
 
# Create a request
request = cerbos_service_pb2.CheckResourcesRequest()
 
# Add principal (user)
principal = request.principal
principal.id = "alice"
principal.roles.append("editor")
 
# Add resource (post)
resource = request.resources.add()
resource.id = "post-123"
resource.kind = "post"
resource.attributes["owner_id"].string_value = "alice"
resource.attributes["title"].string_value = "My Blog"
 
# Add action (delete)
request.actions.append("delete")
 
# Query Cerbos
channel = grpc.aio.aio.secure_channel('localhost:3592', grpc.ssl_channel_credentials())
client = cerbos_service_pb2_grpc.CerbosServiceStub(channel)
response = client.CheckResources(request)
 
# Check result
if response.results[0].actions["delete"] == cerbos_service_pb2.EFFECT_ALLOW:
    delete_post(post_id)
else:
    abort(403)

AWS IAM: if you're on AWS

If your app runs on AWS, AWS IAM (Identity and Access Management) can handle authorization.

Define an IAM policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "posts:Delete",
      "Resource": "arn:aws:posts:us-east-1::posts/*",
      "Condition": {
        "StringEquals": {
          "aws:userid": "${post:owner_id}"
        }
      }
    }
  ]
}

Attach to a role/user. When your Lambda or EC2 instance runs code, the AWS SDK checks the policy.

Benefits:

  • Centralized: one place for all AWS permissions.
  • Hierarchical: organizations, teams, roles.
  • Auditable: CloudTrail logs all permission checks.
  • Integrates with AWS services (S3, DynamoDB, etc.).

Downside: only for AWS services, not for application-specific rules.

Auth0: managed authorization

Auth0 (now Okta) is a managed identity and access management (IAM) platform. It handles authentication and authorization.

Define roles and permissions in Auth0's dashboard:

Role: Editor
  Permissions:
    - read:posts
    - create:posts
    - edit:own_posts
    - delete:own_posts

Role: Viewer
  Permissions:
    - read:posts

Your app queries Auth0:

from auth0 import Auth0
auth0 = Auth0('your-domain.auth0.com', 'your-api-token')
 
# Get user's roles
user_roles = auth0.get_user_roles('alice')  # ['editor']
 
# Get role permissions
permissions = auth0.get_role_permissions('editor')  # ['read:posts', 'create:posts', ...]
 
# Check permission
if 'delete:posts' in permissions:
    delete_post(post_id)

Benefits:

  • Managed: Auth0 handles scaling, backups, updates.
  • Full-featured: roles, permissions, user groups, organization hierarchies.
  • Standards-based: OpenID Connect, OAuth2.

Downside: vendor lock-in, cost per user.

Rule patterns: building intuition

Attribute-based rules

Check user and resource attributes:

allow_edit_document:
  - user.role == "admin"
  - user.department == resource.department AND user.role == "editor"
  - user.id == resource.owner_id AND resource.allow_owner_edit

Temporal rules

Rules that depend on time:

allow_access_during_office_hours:
  - user.role == "admin"  # admins always
  - user.role == "employee" AND now().hour >= 9 AND now().hour <= 17

Relationship-based rules

Rules that depend on relationships:

can_view_team_resources:
  - user.team_id == resource.team_id AND user.role in ["admin", "member"]

Hierarchy-based rules

Rules that respect organizational hierarchy:

can_manage_employees:
  - user.role == "admin"
  - user.department_head == true AND resource.department_id == user.department_id

Testing authorization rules

Authorization rules must be tested:

def test_editor_can_delete_own_post():
    user = User(id="alice", role="editor")
    post = Post(id="123", owner_id="alice")
    assert authz.allow(user, 'delete_post', post)
 
def test_editor_cannot_delete_others_post():
    user = User(id="alice", role="editor")
    post = Post(id="123", owner_id="bob")
    assert not authz.allow(user, 'delete_post', post)
 
def test_admin_can_delete_any_post():
    user = User(id="admin", role="admin")
    post = Post(id="123", owner_id="bob")
    assert authz.allow(user, 'delete_post', post)

With a centralized framework, tests are simple and cover all cases.

Common mistakes

Trusting the frontend to enforce authorization

// WRONG — frontend hides buttons for non-admins
if (user.role === "admin") {
    showDeleteButton();
}

Always enforce on the backend.

Mixing authorization with business logic

# WRONG — authorization is buried in the business logic
def delete_post(post_id):
    post = db.get_post(post_id)
    if request.user.role != "admin" and request.user.id != post.owner_id:
        abort(403)
    # ... business logic

CORRECT — separate concerns

def delete_post(post_id):
    if not authz.allow(request.user, 'delete_post', post):
        abort(403)
    # ... business logic

Not testing authorization rules

Authorization rules are code and must be tested.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What is the main advantage of centralizing authorization rules in a framework?

2. What does Cerbos do?

3. When should you use AWS IAM for authorization?