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
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”What is the difference between ACL, RBAC, and ABAC?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”ACL (Access Control List): each resource has a list of (principal, permission) pairs. Fine-grained but operationally expensive — changing a user's access requires updating every resource they have access to. Scales poorly for millions of resources. RBAC (Role-Based Access Control): users are assigned roles; roles have permissions. Changing permissions for a group of users means updating one role. Standard for enterprise apps. Example: EDITOR role has (Document, WRITE), (Document, READ); all editors inherit these. ABAC (Attribute-Based Access Control): policies reference attributes: "users with department=Finance AND level>=Manager can approve expense reports < $10,000." Most flexible — policies can reference any attribute. Most complex to implement and reason about. Rule of thumb: use RBAC for most apps, ABAC only when role explosion makes RBAC unmanageable (too many roles for too many resource combinations).”}},{“@type”:”Question”,”name”:”How do you cache permission checks to avoid a database lookup on every API request?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Two-level cache: (1) User roles cache: key=user_roles:{user_id}, value=set of (role_id, scope_id) pairs, TTL=5 minutes. Fetch from DB: SELECT role_id, scope_id FROM user_roles WHERE user_id=X AND (expires_at IS NULL OR expires_at > NOW()). (2) Role permissions cache: key=role_perms:{role_id}, value=set of (resource_type, action) pairs, TTL=1 hour. Fetch from DB: SELECT resource_type, action FROM permissions JOIN role_permissions USING (permission_id) WHERE role_id=X. On every API request: check user_roles cache → get role_ids → check role_perms cache for each role → union all permissions → check if requested (resource_type, action) is in the union. All cache reads, no DB hit. Invalidate user_roles cache on role assignment change; invalidate role_perms cache on permission update.”}},{“@type”:”Question”,”name”:”How does role-based access control work in a multi-tenant SaaS application?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Multi-tenant RBAC uses scoped roles: global roles (admin of the whole platform), tenant-scoped roles (admin of tenant X), and resource-scoped roles (editor of document Y). The UserRole table includes a scope_id: NULL for global, tenant_id for tenant-scoped, resource_id for resource-scoped. Permission check for user U accessing resource R in tenant T: (1) Collect all of U's roles: global roles + roles scoped to T + roles scoped to R. (2) Union all permissions for those roles. (3) Check if the required permission is in the union. Tenant isolation: a tenant-scoped ADMIN role only grants admin powers within that tenant's resources. Cross-tenant access is only possible via global roles. Index UserRole on (user_id, scope_id) for efficient lookup.”}},{“@type”:”Question”,”name”:”What is row-level security and when should you use it?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Row-level security (RLS) enforces data access policies at the database layer, ensuring that even if application code has a bug (missing WHERE clause), users cannot see data they shouldn't. In PostgreSQL: CREATE POLICY policy_name ON table USING (condition). The condition references the current user ID (passed as a session variable). Example: documents table with policy USING (owner_id = current_user_id OR document_id IN (user's explicitly shared docs)). Use RLS when: the application has multiple access paths to the same data (API, direct queries, reporting tools), strict compliance requirements (financial data, HIPAA), or the data model naturally segments by user/tenant. RLS adds query overhead (the policy condition is appended to every query). Profile before using in high-QPS paths.”}},{“@type”:”Question”,”name”:”How do you audit who has access to what in a permission system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Effective access report: for any user, query: user's roles (from UserRole), permissions granted by each role (from RolePermission + Permission). Flatten into a list of (resource_type, action) tuples. For resource-specific questions ("who can edit this document?"): query UserRole WHERE scope_id=document_id, union with users having roles that grant global access. For change audit: PermissionAudit table logging every role grant/revoke: (actor, target_user, role, action GRANT/REVOKE, timestamp, reason). Every permission check can also be logged asynchronously (publish to Kafka → audit DB) for compliance: "User X accessed Resource Y at time T." Retain audit logs per compliance requirements (HIPAA=6 years, GDPR=retention policy, SOC2=1 year). Report on "dormant admin access" (users with admin roles who haven't used them in 90 days) for security hygiene.”}}]}
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