Permission and Authorization System Low-Level Design

Permission Models Overview

Three main models:

  • ACL (Access Control List): per-resource list of who can do what. Simple, fine-grained, but scales poorly (checking thousands of ACL entries per request).
  • RBAC (Role-Based Access Control): users are assigned roles; roles have permissions. Scalable, audit-friendly. Works well for enterprise applications.
  • ABAC (Attribute-Based Access Control): policies reference attributes of the user, resource, and environment (e.g., “managers can approve expenses < $1000 in their department”). Most flexible, most complex.

RBAC Data Model

User(user_id, email, name, status)
Role(role_id, name, description, scope ENUM(GLOBAL,TENANT,RESOURCE))
Permission(permission_id, resource_type, action ENUM(READ,WRITE,DELETE,ADMIN))
RolePermission(role_id, permission_id)  -- which permissions a role has
UserRole(user_id, role_id, scope_id, granted_by, granted_at, expires_at)
-- scope_id: NULL for global, tenant_id for tenant-scoped, resource_id for resource-scoped

Permission Check Algorithm

def can(user_id, action, resource_type, resource_id=None):
    # Get all roles for user (global + tenant + resource-scoped)
    roles = get_user_roles(user_id, resource_id)

    # Get all permissions for those roles
    permissions = get_permissions_for_roles(roles)

    # Check if (resource_type, action) is in permissions
    return (resource_type, action) in permissions

Cache at two levels: (1) User roles: Redis SET key=user_roles:{user_id}, TTL=5min. (2) Role permissions: Redis HASH key=role_perms:{role_id}, TTL=1hr. Invalidate on role assignment change or permission update. Avoid DB lookup on every request — permission checks happen on every API call.

Hierarchical Permissions

Resource hierarchy: Organization → Team → Project → Document. If a user has WRITE permission on a Team, they implicitly have WRITE on all Projects and Documents in that team (unless explicitly denied). Implementation: when checking if user can access Document D, also check permissions on D’s parent Project, its parent Team, and its parent Organization. Walk up the hierarchy until a grant is found. Cache the full permission set per (user, resource) with TTL=60s.

Row-Level Security (RLS)

Filter database queries based on the current user’s permissions. Instead of checking permission then fetching data, let the DB enforce access at query time:

-- PostgreSQL Row Level Security
CREATE POLICY user_documents ON documents
    USING (owner_id = current_setting('app.current_user_id')::uuid
           OR document_id IN (
               SELECT resource_id FROM user_permissions
               WHERE user_id = current_setting('app.current_user_id')::uuid
               AND action = 'READ'
           ));

Set the current user ID before each query: SET LOCAL app.current_user_id = ‘{user_id}’. RLS prevents accidental data exposure from missing WHERE clauses in application code.

Permission Inheritance and Scope

Three role scopes:

  • Global roles: apply across all resources (e.g., SUPER_ADMIN, BILLING_ADMIN)
  • Tenant-scoped roles: apply within a specific tenant/organization (e.g., ORG_ADMIN for organization X)
  • Resource-scoped roles: apply to a specific resource (e.g., DOCUMENT_EDITOR for document Y)

Permission resolution: global roles take precedence, then tenant-scoped, then resource-scoped. Deny overrides grant at the same level (explicit deny beats any grant).

Audit Trail

PermissionAudit(audit_id, user_id, action, resource_type, resource_id,
                granted BOOL, reason, ip_address, created_at)

Log every permission check result (ALLOW/DENY) for security auditing. Store asynchronously (Kafka → audit DB) to avoid adding latency to API requests. Retain audit logs for 1-7 years depending on compliance requirements (HIPAA: 6 years, SOC2: 1 year).

Key Design Decisions

  • RBAC for most enterprise apps — ABAC only if role explosion is unavoidable
  • Cache user roles and role permissions to avoid per-request DB lookups
  • RLS for defense in depth — DB-level enforcement catches application bugs
  • Explicit deny takes precedence over grant to enable exception handling
  • Scoped roles (global/tenant/resource) handle multi-tenant SaaS without separate permission tables per tenant

Atlassian products (Jira, Confluence) use complex permission systems. See common questions for Atlassian interview: permission and authorization system design.

Google system design covers permission systems and access control. Review design patterns for Google interview: permission system and RBAC design.

Databricks system design covers multi-tenant permissions and data access control. See patterns for Databricks interview: permission and authorization system design.

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

Scroll to Top