Loading learning content...
Once a user's identity is verified through authentication, a second critical question arises: What are you allowed to do? Authorization—the process of determining access rights—is the gatekeeper that stands between authenticated users and the resources or actions they seek to access.
Authorization design is deceptively complex. Simple applications might get away with basic role checks, but sophisticated systems require nuanced permission models, hierarchical roles, resource-level access control, and policy engines capable of expressing complex business rules.
This page dissects authorization design patterns from foundational concepts through advanced architectures, equipping you with the knowledge to design access control systems that are both secure and maintainable. We will explore the evolution from primitive role checks to sophisticated attribute-based and policy-based access control, providing implementations that scale from startups to enterprises.
By the end of this page, you will understand the major authorization models (DAC, MAC, RBAC, ABAC), how to design permission and role systems with proper hierarchies, implement policy-based access control, and integrate authorization checks throughout your object-oriented designs.
Authorization determines what actions a verified identity can perform on which resources. Before exploring design patterns, we must understand the fundamental concepts and vocabulary of access control.
Core Authorization Concepts:
The Authorization Decision:
Every authorization check asks: "Can subject S perform action A on resource R given context C?"
The context might include:
Authorization vs. Validation:
It's important to distinguish authorization from business validation:
Both may prevent an action, but for different reasons. Keep these concerns separate in your design.
| Model | Description | Best For | Complexity |
|---|---|---|---|
| DAC (Discretionary) | Resource owners control access | File systems, simple apps | Low |
| MAC (Mandatory) | System-enforced labels/clearances | Military, government | High |
| RBAC (Role-Based) | Roles define permission sets | Enterprise applications | Medium |
| ABAC (Attribute-Based) | Attributes and policies define access | Complex, dynamic rules | High |
| ReBAC (Relationship-Based) | Relationships define access | Social, collaborative apps | Medium-High |
Access control has evolved from simple ownership (DAC) through role-based systems (RBAC) to sophisticated attribute and policy-based models (ABAC). Modern systems often combine multiple models—using RBAC as a foundation enhanced with attribute-based policies for fine-grained control.
Role-Based Access Control (RBAC) is the most widely adopted authorization model in enterprise software. Users are assigned roles, and roles are assigned permissions. This layer of indirection simplifies administration and provides natural alignment with organizational structures.
RBAC Core Components:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
// Permission represents a specific allowed actionclass Permission { constructor( public readonly id: string, public readonly resource: string, // e.g., "document", "user", "order" public readonly action: string, // e.g., "read", "write", "delete", "approve" public readonly description: string ) {} // Permission string format: "resource:action" toString(): string { return `${this.resource}:${this.action}`; } static fromString(permStr: string): Permission { const [resource, action] = permStr.split(':'); return new Permission(permStr, resource, action, ''); } // Check if this permission matches a requested permission // Supports wildcards: "document:*" matches "document:read", "document:write", etc. matches(resource: string, action: string): boolean { const resourceMatches = this.resource === '*' || this.resource === resource; const actionMatches = this.action === '*' || this.action === action; return resourceMatches && actionMatches; }} // Role is a named collection of permissionsclass Role { private readonly _permissions: Set<Permission>; private readonly _parentRoles: Set<Role>; constructor( public readonly id: string, public readonly name: string, public readonly description: string, permissions: Permission[] = [], parentRoles: Role[] = [] ) { this._permissions = new Set(permissions); this._parentRoles = new Set(parentRoles); } // Get all permissions including inherited getAllPermissions(): Set<Permission> { const allPerms = new Set(this._permissions); // Recursively include parent role permissions for (const parent of this._parentRoles) { for (const perm of parent.getAllPermissions()) { allPerms.add(perm); } } return allPerms; } hasPermission(resource: string, action: string): boolean { for (const perm of this.getAllPermissions()) { if (perm.matches(resource, action)) { return true; } } return false; } // Direct permission management addPermission(permission: Permission): void { this._permissions.add(permission); } removePermission(permissionId: string): boolean { for (const perm of this._permissions) { if (perm.id === permissionId) { this._permissions.delete(perm); return true; } } return false; } // Role hierarchy management addParentRole(role: Role): void { // Prevent circular inheritance if (this.wouldCreateCycle(role)) { throw new CircularRoleHierarchyError( `Adding ${role.name} as parent of ${this.name} would create a cycle` ); } this._parentRoles.add(role); } private wouldCreateCycle(proposedParent: Role): boolean { // Check if proposed parent inherits from this role const visited = new Set<string>(); const queue: Role[] = [proposedParent]; while (queue.length > 0) { const current = queue.shift()!; if (current.id === this.id) return true; if (visited.has(current.id)) continue; visited.add(current.id); queue.push(...current._parentRoles); } return false; }} // User with role assignmentsinterface UserWithRoles { readonly id: string; readonly roles: Role[]; hasPermission(resource: string, action: string): boolean; hasRole(roleName: string): boolean; getAllPermissions(): Permission[];} class AuthenticatedUser implements UserWithRoles { constructor( public readonly id: string, public readonly roles: Role[] ) {} hasPermission(resource: string, action: string): boolean { return this.roles.some(role => role.hasPermission(resource, action)); } hasRole(roleName: string): boolean { return this.roles.some(role => role.name === roleName); } getAllPermissions(): Permission[] { const allPerms = new Set<Permission>(); for (const role of this.roles) { for (const perm of role.getAllPermissions()) { allPerms.add(perm); } } return Array.from(allPerms); }}Role Hierarchy Design:
RBAC systems often feature hierarchical roles where higher-level roles inherit permissions from lower-level roles. This models real-world organizational structures:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Building a role hierarchy for document managementclass RoleBuilder { static buildDocumentManagementRoles(): Map<string, Role> { // Base permissions const readDoc = new Permission('doc:read', 'document', 'read', 'Read documents'); const writeDoc = new Permission('doc:write', 'document', 'write', 'Edit documents'); const deleteDoc = new Permission('doc:delete', 'document', 'delete', 'Delete documents'); const publishDoc = new Permission('doc:publish', 'document', 'publish', 'Publish documents'); const manageUsers = new Permission('user:manage', 'user', 'manage', 'Manage users'); // Viewer: can only read const viewer = new Role('viewer', 'Viewer', 'Can view documents', [readDoc]); // Editor: inherits from Viewer, adds write const editor = new Role( 'editor', 'Editor', 'Can view and edit documents', [writeDoc], [viewer] ); // Publisher: inherits from Editor, adds publish const publisher = new Role( 'publisher', 'Publisher', 'Can publish documents', [publishDoc], [editor] ); // Admin: inherits from Publisher, adds delete and user management const admin = new Role( 'admin', 'Admin', 'Full document and user management', [deleteDoc, manageUsers], [publisher] ); return new Map([ ['viewer', viewer], ['editor', editor], ['publisher', publisher], ['admin', admin], ]); }} // Example: Admin has all permissions via inheritanceconst roles = RoleBuilder.buildDocumentManagementRoles();const admin = roles.get('admin')!; console.log(admin.hasPermission('document', 'read')); // true (inherited from Viewer)console.log(admin.hasPermission('document', 'write')); // true (inherited from Editor)console.log(admin.hasPermission('document', 'publish')); // true (inherited from Publisher)console.log(admin.hasPermission('document', 'delete')); // true (direct)console.log(admin.hasPermission('user', 'manage')); // true (direct)Follow the principle of least privilege: assign the minimum roles necessary. Use role hierarchy to avoid permission duplication. Create functional roles (Editor, Publisher) rather than job titles (Junior Developer). Review role assignments regularly. Implement separation of duties for critical operations.
RBAC works well for coarse-grained access control, but complex systems require finer granularity. Attribute-Based Access Control (ABAC) evaluates access decisions based on attributes of the subject, resource, action, and context.
ABAC Components:
Policy Structure:
ABAC policies define rules that combine attributes with logical operators:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
// Attribute typesinterface Attributes { [key: string]: AttributeValue;} type AttributeValue = string | number | boolean | string[] | Date; // Access request with all relevant attributesinterface AccessRequest { readonly subject: SubjectAttributes; readonly resource: ResourceAttributes; readonly action: ActionAttributes; readonly context: ContextAttributes;} interface SubjectAttributes extends Attributes { id: string; department?: string; clearanceLevel?: number; roles?: string[]; teamId?: string; isManager?: boolean;} interface ResourceAttributes extends Attributes { id: string; type: string; owner: string; sensitivity?: 'public' | 'internal' | 'confidential' | 'secret'; department?: string; status?: string; teamId?: string;} interface ContextAttributes extends Attributes { timestamp: Date; ipAddress: string; deviceTrusted?: boolean; riskScore?: number; location?: string;} // Policy definitioninterface Policy { readonly id: string; readonly name: string; readonly description: string; readonly effect: 'PERMIT' | 'DENY'; readonly priority: number; readonly target: PolicyTarget; readonly conditions: Condition[];} interface PolicyTarget { readonly resourceTypes?: string[]; readonly actions?: string[]; readonly subjectRoles?: string[];} interface Condition { readonly attribute: string; // e.g., "subject.department", "resource.sensitivity" readonly operator: ConditionOperator; readonly value: AttributeValue;} type ConditionOperator = | 'EQUALS' | 'NOT_EQUALS' | 'GREATER_THAN' | 'LESS_THAN' | 'GREATER_EQUAL' | 'LESS_EQUAL' | 'CONTAINS' | 'IN' | 'NOT_IN' | 'MATCHES_REGEX' | 'EQUALS_ATTRIBUTE'; // Compare two attributes // Policy Decision Point (PDP)class PolicyDecisionPoint { constructor( private readonly policyRepository: PolicyRepository, private readonly attributeResolver: AttributeResolver ) {} async evaluate(request: AccessRequest): Promise<AccessDecision> { // Get applicable policies const policies = await this.policyRepository.findApplicablePolicies( request.resource.type, request.action.name ); // No applicable policies = deny by default if (policies.length === 0) { return AccessDecision.deny('No applicable policies found'); } // Evaluate policies in priority order const sortedPolicies = policies.sort((a, b) => a.priority - b.priority); let matchedDeny: Policy | null = null; let matchedPermit: Policy | null = null; for (const policy of sortedPolicies) { if (this.policyApplies(policy, request)) { const conditionsMet = this.evaluateConditions(policy.conditions, request); if (conditionsMet) { if (policy.effect === 'DENY') { matchedDeny = policy; break; // Deny takes precedence } else { matchedPermit = matchedPermit || policy; } } } } // Apply conflict resolution: Deny overrides Permit if (matchedDeny) { return AccessDecision.deny(`Denied by policy: ${matchedDeny.name}`); } if (matchedPermit) { return AccessDecision.permit(`Permitted by policy: ${matchedPermit.name}`); } return AccessDecision.deny('No matching permit policy'); } private policyApplies(policy: Policy, request: AccessRequest): boolean { const target = policy.target; // Check resource type if (target.resourceTypes && !target.resourceTypes.includes(request.resource.type)) { return false; } // Check action if (target.actions && !target.actions.includes(request.action.name)) { return false; } // Check subject roles if (target.subjectRoles) { const userRoles = request.subject.roles || []; if (!target.subjectRoles.some(r => userRoles.includes(r))) { return false; } } return true; } private evaluateConditions(conditions: Condition[], request: AccessRequest): boolean { return conditions.every(condition => this.evaluateCondition(condition, request) ); } private evaluateCondition(condition: Condition, request: AccessRequest): boolean { const actualValue = this.resolveAttribute(condition.attribute, request); const expectedValue = condition.operator === 'EQUALS_ATTRIBUTE' ? this.resolveAttribute(condition.value as string, request) : condition.value; switch (condition.operator) { case 'EQUALS': return actualValue === expectedValue; case 'NOT_EQUALS': return actualValue !== expectedValue; case 'GREATER_THAN': return (actualValue as number) > (expectedValue as number); case 'LESS_THAN': return (actualValue as number) < (expectedValue as number); case 'CONTAINS': return (actualValue as string[]).includes(expectedValue as string); case 'IN': return (expectedValue as string[]).includes(actualValue as string); case 'EQUALS_ATTRIBUTE': return actualValue === expectedValue; default: return false; } } private resolveAttribute(path: string, request: AccessRequest): AttributeValue | undefined { const [category, ...rest] = path.split('.'); const key = rest.join('.'); switch (category) { case 'subject': return request.subject[key]; case 'resource': return request.resource[key]; case 'action': return request.action[key]; case 'context': return request.context[key]; default: return undefined; } }}Example ABAC Policies:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// Example policies for a document management system const abacPolicies: Policy[] = [ // Policy 1: Users can read documents in their own department { id: 'pol-001', name: 'Department Document Access', description: 'Users can read documents belonging to their department', effect: 'PERMIT', priority: 100, target: { resourceTypes: ['document'], actions: ['read'], }, conditions: [ { attribute: 'subject.department', operator: 'EQUALS_ATTRIBUTE', value: 'resource.department', }, ], }, // Policy 2: Managers can read all documents in their team { id: 'pol-002', name: 'Manager Team Access', description: 'Managers can read all documents from their team members', effect: 'PERMIT', priority: 90, target: { resourceTypes: ['document'], actions: ['read'], }, conditions: [ { attribute: 'subject.isManager', operator: 'EQUALS', value: true }, { attribute: 'subject.teamId', operator: 'EQUALS_ATTRIBUTE', value: 'resource.teamId', }, ], }, // Policy 3: Deny access to confidential documents after hours { id: 'pol-003', name: 'After Hours Confidential Restriction', description: 'Block confidential document access outside business hours', effect: 'DENY', priority: 10, // High priority (evaluated first) target: { resourceTypes: ['document'], }, conditions: [ { attribute: 'resource.sensitivity', operator: 'IN', value: ['confidential', 'secret'] }, { attribute: 'context.timestamp', operator: 'OUTSIDE_HOURS', value: '09:00-18:00' }, ], }, // Policy 4: Deny access from untrusted devices for sensitive data { id: 'pol-004', name: 'Trusted Device Requirement', description: 'Sensitive data requires trusted device', effect: 'DENY', priority: 5, target: { resourceTypes: ['document', 'report'], }, conditions: [ { attribute: 'resource.sensitivity', operator: 'IN', value: ['confidential', 'secret'] }, { attribute: 'context.deviceTrusted', operator: 'EQUALS', value: false }, ], }, // Policy 5: Document owners have full access { id: 'pol-005', name: 'Owner Full Access', description: 'Document owners can perform any action', effect: 'PERMIT', priority: 50, target: { resourceTypes: ['document'], }, conditions: [ { attribute: 'subject.id', operator: 'EQUALS_ATTRIBUTE', value: 'resource.owner', }, ], },];ABAC provides powerful, fine-grained control but introduces complexity. Policies can interact in unexpected ways. Test policies thoroughly with diverse scenarios. Consider using policy analysis tools to detect conflicts. Start with RBAC and add ABAC selectively for specific requirements.
A policy engine is useless without enforcement points that intercept operations and consult the policy decision point. Policy Enforcement Points (PEPs) integrate authorization into application flow.
Enforcement Strategies:
| Strategy | Description | Pros | Cons |
|---|---|---|---|
| Decorator Pattern | Wrap methods with authorization checks | Non-invasive, composable | Overhead, reflection complexity |
| Interceptor/Middleware | Check authorization before request handling | Centralized, consistent | May lack context for fine-grained checks |
| Explicit Checks | Call authorization service directly | Clear, debuggable | Scattered checks, easy to miss |
| Aspect-Oriented | Weave authorization via AOP | Separation of concerns | Magic behavior, harder debugging |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
// Pattern 1: Decorator-based enforcementfunction RequirePermission(resource: string, action: string) { return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { const context = getCurrentSecurityContext(); if (!context.hasPermission(resource, action)) { throw new AccessDeniedException( `Permission ${resource}:${action} required` ); } return originalMethod.apply(this, args); }; return descriptor; };} // Usageclass DocumentService { @RequirePermission('document', 'read') async getDocument(id: string): Promise<Document> { return this.repository.findById(id); } @RequirePermission('document', 'write') async updateDocument(id: string, updates: DocumentUpdates): Promise<Document> { return this.repository.update(id, updates); } @RequirePermission('document', 'delete') async deleteDocument(id: string): Promise<void> { return this.repository.delete(id); }} // Pattern 2: Resource-level authorization with ABACclass AuthorizedDocumentService { constructor( private readonly documentRepository: DocumentRepository, private readonly policyDecisionPoint: PolicyDecisionPoint, private readonly securityContext: SecurityContextProvider ) {} async getDocument(id: string): Promise<Document> { const document = await this.documentRepository.findById(id); if (!document) { throw new DocumentNotFoundException(id); } // Build access request with full context const decision = await this.policyDecisionPoint.evaluate({ subject: this.buildSubjectAttributes(), resource: this.buildResourceAttributes(document), action: { name: 'read', type: 'query' }, context: this.buildContextAttributes(), }); if (!decision.isPermitted) { throw new AccessDeniedException(decision.reason); } return document; } async updateDocument(id: string, updates: DocumentUpdates): Promise<Document> { const document = await this.documentRepository.findById(id); if (!document) { throw new DocumentNotFoundException(id); } const decision = await this.policyDecisionPoint.evaluate({ subject: this.buildSubjectAttributes(), resource: this.buildResourceAttributes(document), action: { name: 'write', type: 'mutation' }, context: this.buildContextAttributes(), }); if (!decision.isPermitted) { throw new AccessDeniedException(decision.reason); } return this.documentRepository.update(id, updates); } // For list operations, filter based on authorization async listDocuments(filters: DocumentFilters): Promise<Document[]> { const allDocuments = await this.documentRepository.find(filters); const subject = this.buildSubjectAttributes(); const context = this.buildContextAttributes(); // Filter to only authorized documents const authorizedDocs: Document[] = []; for (const doc of allDocuments) { const decision = await this.policyDecisionPoint.evaluate({ subject, resource: this.buildResourceAttributes(doc), action: { name: 'read', type: 'query' }, context, }); if (decision.isPermitted) { authorizedDocs.push(doc); } } return authorizedDocs; } private buildSubjectAttributes(): SubjectAttributes { const user = this.securityContext.getCurrentUser(); return { id: user.id, department: user.department, roles: user.roles, teamId: user.teamId, isManager: user.isManager, clearanceLevel: user.clearanceLevel, }; } private buildResourceAttributes(doc: Document): ResourceAttributes { return { id: doc.id, type: 'document', owner: doc.ownerId, sensitivity: doc.sensitivity, department: doc.departmentId, status: doc.status, teamId: doc.teamId, }; } private buildContextAttributes(): ContextAttributes { const ctx = this.securityContext.getRequestContext(); return { timestamp: new Date(), ipAddress: ctx.ipAddress, deviceTrusted: ctx.deviceTrusted, location: ctx.location, riskScore: ctx.riskScore, }; }}Don't just enforce authorization—also provide 'can I?' queries for UI enablement. This allows hiding or disabling buttons for actions the user cannot perform, improving UX. Use the same policy engine for both enforcement and enablement to ensure consistency.
A complete authorization subsystem requires multiple components working together. Following separation of concerns, we structure the authorization domain into distinct services:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
// Authorization service facade - main entry pointclass AuthorizationService { constructor( private readonly rbacEngine: RbacEngine, private readonly abacEngine: PolicyDecisionPoint, private readonly permissionCache: PermissionCache, private readonly auditLogger: AuthorizationAuditLogger ) {} // Check if user can perform action (simple RBAC) async hasPermission( userId: string, resource: string, action: string ): Promise<boolean> { // Check cache first const cacheKey = `${userId}:${resource}:${action}`; const cached = await this.permissionCache.get(cacheKey); if (cached !== undefined) { return cached; } // RBAC check const hasRbacPermission = await this.rbacEngine.hasPermission( userId, resource, action ); // Cache result await this.permissionCache.set(cacheKey, hasRbacPermission, 300); // 5 min TTL return hasRbacPermission; } // Full access decision with ABAC async checkAccess(request: AccessRequest): Promise<AccessDecision> { // Log access check (without sensitive data) await this.auditLogger.logAccessCheck(request); // First check RBAC for performance (cheaper than ABAC) const hasRolePermission = await this.rbacEngine.hasPermission( request.subject.id, request.resource.type, request.action.name ); if (!hasRolePermission) { const decision = AccessDecision.deny('Insufficient role permissions'); await this.auditLogger.logAccessDecision(request, decision); return decision; } // Then apply ABAC policies for fine-grained control const decision = await this.abacEngine.evaluate(request); await this.auditLogger.logAccessDecision(request, decision); return decision; } // Bulk check for UI pre-authorization async getAuthorizedActions( userId: string, resource: ResourceAttributes ): Promise<AuthorizedActions> { const actions = ['read', 'write', 'delete', 'share', 'publish']; const result: AuthorizedActions = { resourceId: resource.id, actions: {} }; const subject = await this.rbacEngine.getSubjectAttributes(userId); const context = this.getCurrentContext(); // Check each action await Promise.all(actions.map(async (action) => { const decision = await this.abacEngine.evaluate({ subject, resource, action: { name: action, type: 'query' }, context, }); result.actions[action] = decision.isPermitted; })); return result; } // Invalidate cache on permission changes async invalidateUserPermissions(userId: string): Promise<void> { await this.permissionCache.invalidateByPrefix(`${userId}:`); }} // RBAC Engine for role-based checksclass RbacEngine { constructor( private readonly userRepository: UserRepository, private readonly roleRepository: RoleRepository ) {} async hasPermission(userId: string, resource: string, action: string): Promise<boolean> { const user = await this.userRepository.findById(userId); if (!user) return false; const roles = await this.roleRepository.findByIds(user.roleIds); for (const role of roles) { if (role.hasPermission(resource, action)) { return true; } } return false; } async getSubjectAttributes(userId: string): Promise<SubjectAttributes> { const user = await this.userRepository.findById(userId); return { id: userId, department: user?.department, roles: user?.roleNames, teamId: user?.teamId, isManager: user?.isManager, clearanceLevel: user?.clearanceLevel, }; }} // Role management serviceclass RoleManagementService { constructor( private readonly roleRepository: RoleRepository, private readonly userRoleRepository: UserRoleRepository, private readonly eventPublisher: EventPublisher, private readonly auditLogger: RoleAuditLogger ) {} async assignRoleToUser(userId: string, roleId: string, assignedBy: string): Promise<void> { const existing = await this.userRoleRepository.findByUserAndRole(userId, roleId); if (existing) { throw new RoleAlreadyAssignedException(userId, roleId); } await this.userRoleRepository.create({ userId, roleId, assignedBy, assignedAt: new Date(), }); await this.eventPublisher.publish(new RoleAssignedEvent(userId, roleId, assignedBy)); await this.auditLogger.logRoleAssignment(userId, roleId, assignedBy); } async removeRoleFromUser(userId: string, roleId: string, removedBy: string): Promise<void> { await this.userRoleRepository.delete(userId, roleId); await this.eventPublisher.publish(new RoleRemovedEvent(userId, roleId, removedBy)); await this.auditLogger.logRoleRemoval(userId, roleId, removedBy); } async createRole(role: CreateRoleDto, createdBy: string): Promise<Role> { const existing = await this.roleRepository.findByName(role.name); if (existing) { throw new RoleAlreadyExistsException(role.name); } const newRole = await this.roleRepository.create(role); await this.auditLogger.logRoleCreation(newRole.id, createdBy); return newRole; } async updateRolePermissions( roleId: string, permissions: Permission[], updatedBy: string ): Promise<Role> { const role = await this.roleRepository.findById(roleId); if (!role) { throw new RoleNotFoundException(roleId); } const previousPermissions = role.getAllPermissions(); await this.roleRepository.updatePermissions(roleId, permissions); await this.auditLogger.logPermissionChange( roleId, previousPermissions, permissions, updatedBy ); // Invalidate cached permissions for all users with this role await this.eventPublisher.publish(new RolePermissionsChangedEvent(roleId)); return this.roleRepository.findById(roleId); }}Authorization decisions are prime candidates for caching due to high frequency. But cache invalidation is critical—stale permissions are security vulnerabilities. Invalidate on: role assignment changes, permission updates, user attribute changes. Consider short TTLs (5-15 minutes) as a safety net.
Relationship-Based Access Control (ReBAC) determines access based on the relationship between subjects and resources. This is particularly powerful for collaborative applications where access is derived from relationships like ownership, team membership, or sharing.
ReBAC Example: Google Drive-like Access Control
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
// Relationship types in the systemenum RelationType { OWNER = 'owner', // Full control EDITOR = 'editor', // Can read and write VIEWER = 'viewer', // Can only read PARENT = 'parent', // Folder contains document MEMBER = 'member', // User is member of team} // Relationship tuple: subject#relation@objectinterface RelationTuple { subject: SubjectRef; relation: RelationType; object: ObjectRef;} type SubjectRef = | { type: 'user'; id: string } | { type: 'group'; id: string } | { type: 'object'; objectType: string; objectId: string; relation: RelationType }; interface ObjectRef { type: string; // e.g., 'document', 'folder', 'team' id: string;} // ReBAC permission is defined by combining relationsinterface ComputedPermission { permission: string; // e.g., 'can_read', 'can_write', 'can_delete' derivedFrom: RelationType[]; // Relations that grant this permission} class RelationshipAccessControl { private readonly permissionMap: Map<string, ComputedPermission[]> = new Map([ ['document', [ { permission: 'can_read', derivedFrom: [RelationType.OWNER, RelationType.EDITOR, RelationType.VIEWER] }, { permission: 'can_write', derivedFrom: [RelationType.OWNER, RelationType.EDITOR] }, { permission: 'can_delete', derivedFrom: [RelationType.OWNER] }, { permission: 'can_share', derivedFrom: [RelationType.OWNER] }, ]], ['folder', [ { permission: 'can_read', derivedFrom: [RelationType.OWNER, RelationType.EDITOR, RelationType.VIEWER] }, { permission: 'can_create_child', derivedFrom: [RelationType.OWNER, RelationType.EDITOR] }, ]], ]); constructor( private readonly relationStore: RelationStore, ) {} async check( subject: SubjectRef, permission: string, object: ObjectRef ): Promise<boolean> { const computedPerms = this.permissionMap.get(object.type); if (!computedPerms) return false; const perm = computedPerms.find(p => p.permission === permission); if (!perm) return false; // Check direct relations for (const relation of perm.derivedFrom) { if (await this.hasRelation(subject, relation, object)) { return true; } } // Check inherited relations (e.g., from parent folder) const parentRelations = await this.relationStore.findBySubjectAndRelation( { type: 'object', objectType: object.type, objectId: object.id, relation: RelationType.PARENT }, RelationType.PARENT ); for (const parentRel of parentRelations) { // Recursively check permission on parent if (await this.check(subject, permission, parentRel.object)) { return true; } } // Check group membership if (subject.type === 'user') { const groupMemberships = await this.relationStore.findGroupsForUser(subject.id); for (const groupId of groupMemberships) { if (await this.check({ type: 'group', id: groupId }, permission, object)) { return true; } } } return false; } private async hasRelation( subject: SubjectRef, relation: RelationType, object: ObjectRef ): Promise<boolean> { return this.relationStore.exists({ subject, relation, object, }); } // Add a new relation (e.g., share document with user) async addRelation(tuple: RelationTuple, grantedBy: string): Promise<void> { await this.relationStore.create({ ...tuple, grantedBy, grantedAt: new Date(), }); } // Remove a relation async removeRelation(tuple: RelationTuple): Promise<void> { await this.relationStore.delete(tuple); } // Get all users who can access an object with given permission async listUsersWithPermission( permission: string, object: ObjectRef ): Promise<string[]> { const computedPerms = this.permissionMap.get(object.type); if (!computedPerms) return []; const perm = computedPerms.find(p => p.permission === permission); if (!perm) return []; const users = new Set<string>(); for (const relation of perm.derivedFrom) { const relations = await this.relationStore.findByObjectAndRelation(object, relation); for (const rel of relations) { if (rel.subject.type === 'user') { users.add(rel.subject.id); } else if (rel.subject.type === 'group') { const groupMembers = await this.relationStore.getGroupMembers(rel.subject.id); groupMembers.forEach(id => users.add(id)); } } } return Array.from(users); }} // Usage exampleasync function shareDocumentExample(rac: RelationshipAccessControl) { // Alice owns a document await rac.addRelation({ subject: { type: 'user', id: 'alice' }, relation: RelationType.OWNER, object: { type: 'document', id: 'doc-123' }, }, 'system'); // Alice shares with Bob as editor await rac.addRelation({ subject: { type: 'user', id: 'bob' }, relation: RelationType.EDITOR, object: { type: 'document', id: 'doc-123' }, }, 'alice'); // Check permissions console.log(await rac.check( { type: 'user', id: 'alice' }, 'can_delete', { type: 'document', id: 'doc-123' } )); // true - Alice is owner console.log(await rac.check( { type: 'user', id: 'bob' }, 'can_write', { type: 'document', id: 'doc-123' } )); // true - Bob is editor console.log(await rac.check( { type: 'user', id: 'bob' }, 'can_delete', { type: 'document', id: 'doc-123' } )); // false - Bob is not owner}ReBAC excels for: collaborative applications (Google Docs, Slack), social networks (friends, followers), organizational hierarchies (manager chain), and resource hierarchies (folders containing files). It naturally models 'share with' and 'belongs to' relationships that are awkward in pure RBAC.
Authorization vulnerabilities can be as devastating as authentication failures. Following best practices ensures your access control is robust:
Broken Access Control is OWASP's #1 vulnerability. Common issues: IDOR (Insecure Direct Object References) where users manipulate IDs to access others' data, missing function-level access control, privilege escalation through parameter tampering, and inconsistent enforcement across endpoints.
Authorization design patterns control what authenticated users can do. Let's consolidate the key architectural insights:
What's next:
With authentication and authorization patterns covered, the next page explores secure object design—how to structure your classes themselves to resist attack and minimize vulnerability surface.
You now understand major authorization models and how to implement them. You can design RBAC hierarchies, implement ABAC policy engines, and integrate ReBAC for relationship-based access. Apply these patterns to build robust access control systems.