We use cookies to enhance your experience on the site
CodeWorlds

Role-based Authorization - hierarchy in the Empire

Hierarchy organizer! Architect Vitruvius has noticed that although all legionaries now have access to the camp, not everyone should have access to all rooms. It's time to learn Role-based Authorization - a system that ensures each legionary has access only to what their rank in the Roman army entitles them to!

What is Authorization?

Imagine you are a prefect responsible for order in the legionary camp. Every legionary has already passed through the gate (authentication), but now you must decide which parts of the camp they can access:

  • Tiro (recruit) - can clean and help in the field kitchen
  • Miles (soldier) - can stand guard and perform basic tasks
  • Decanus (leader of ten) - has access to the armory and warehouses
  • Optio (deputy centurion) - manages supplies and training
  • Centurion (century commander) - can lead the unit and plan campaigns
  • Legatus (legion commander) - has access to everything in the entire legion

Authorization is the process of determining what an authenticated user can access.

Role-based Access Control (RBAC)

Role definition

1// enums/roles.enum.ts
2export enum Role {
3 TIRO = 'TIRO',             // Recruit
4 MILES = 'MILES',           // Soldier
5 DECANUS = 'DECANUS',       // Leader of ten
6 OPTIO = 'OPTIO',           // Deputy centurion
7 CENTURION = 'CENTURION',   // Century commander
8 LEGATUS = 'LEGATUS',       // Legion commander
9 ADMIN = 'ADMIN',           // Super admin of the Empire
10}
11
12// Role hierarchy (higher ranks inherit permissions of lower ones)
13export const ROLE_HIERARCHY = {
14 [Role.ADMIN]: 100,
15 [Role.LEGATUS]: 90,
16 [Role.CENTURION]: 80,
17 [Role.OPTIO]: 70,
18 [Role.DECANUS]: 60,
19 [Role.MILES]: 50,
20 [Role.TIRO]: 40,
21};
22
23export function hasPermission(userRole: Role, requiredRole: Role): boolean {
24 return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole];
25}

Roles Decorator

1// auth/decorators/roles.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3import { Role } from '../enums/roles.enum';
4
5export const ROLES_KEY = 'roles';
6export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
7
8// Aliases for convenience - Roman army ranks
9export const RequireLegatus = () => Roles(Role.LEGATUS);
10export const RequireCenturion = () => Roles(Role.CENTURION, Role.LEGATUS);
11export const RequireOptio = () => Roles(Role.OPTIO, Role.CENTURION, Role.LEGATUS);
12export const RequireDecanus = () => Roles(Role.DECANUS, Role.OPTIO, Role.CENTURION, Role.LEGATUS);
13export const RequireMiles = () => Roles(Role.MILES, Role.DECANUS, Role.OPTIO, Role.CENTURION, Role.LEGATUS);

Roles Guard

1// auth/guards/roles.guard.ts
2import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
3import { Reflector } from '@nestjs/core';
4import { Role, hasPermission } from '../enums/roles.enum';
5import { ROLES_KEY } from '../decorators/roles.decorator';
6import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
7
8@Injectable()
9export class RolesGuard implements CanActivate {
10 constructor(private reflector: Reflector) {}
11
12 canActivate(context: ExecutionContext): boolean {
13 // Check if the endpoint is public
14 const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
15 context.getHandler(),
16 context.getClass(),
17 ]);
18 
19 if (isPublic) {
20 return true;
21 }
22
23 // Get the required roles
24 const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
25 context.getHandler(),
26 context.getClass(),
27 ]);
28 
29 if (!requiredRoles) {
30 return true; // No role restrictions
31 }
32
33 const { user } = context.switchToHttp().getRequest();
34 
35 if (!user) {
36 throw new ForbiddenException('No user information available!');
37 }
38
39 // Check if the user has one of the required roles
40 const hasRole = requiredRoles.some(role => 
41 hasPermission(user.role as Role, role)
42 );
43
44 if (!hasRole) {
45 throw new ForbiddenException(
46 `Insufficient permissions! Required role: ${requiredRoles.join(' or ')}. Your role: ${user.role}`
47 );
48 }
49
50 return true;
51 }
52}

Permission-based Authorization

Permission definition

1// enums/permissions.enum.ts
2export enum Permission {
3 // User management
4 USER_CREATE = 'user:create',
5 USER_READ = 'user:read',
6 USER_UPDATE = 'user:update',
7 USER_DELETE = 'user:delete',
8 
9 // Legion resource management
10 RESOURCE_CREATE = 'resource:create',
11 RESOURCE_READ = 'resource:read',
12 RESOURCE_UPDATE = 'resource:update',
13 RESOURCE_DELETE = 'resource:delete',
14 RESOURCE_TRANSFER = 'resource:transfer',
15
16 // Cohort management
17 COHORT_PATROL = 'cohort:patrol',
18 COHORT_COMMAND = 'cohort:command',
19 COHORT_MAINTENANCE = 'cohort:maintenance',
20
21 // Armory management
22 ARMORY_ACCESS = 'armory:access',
23 ARMORY_MAINTAIN = 'armory:maintain',
24
25 // Special permissions
26 ACCESS_PRAETORIUM = 'access:praetorium',
27 ACCESS_TREASURY = 'access:treasury',
28 ACCESS_ARMORY = 'access:armory',
29 ACCESS_STRATEGIC_ROOM = 'access:strategic_room',
30}
31
32// Mapping roles to permissions - Roman army hierarchy
33export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
34 [Role.TIRO]: [
35 Permission.RESOURCE_READ,
36 ],
37
38 [Role.MILES]: [
39 Permission.RESOURCE_READ,
40 Permission.COHORT_MAINTENANCE,
41 ],
42
43 [Role.DECANUS]: [
44 Permission.RESOURCE_READ,
45 Permission.COHORT_MAINTENANCE,
46 Permission.ARMORY_ACCESS,
47 Permission.ARMORY_MAINTAIN,
48 Permission.ACCESS_ARMORY,
49 ],
50
51 [Role.OPTIO]: [
52 Permission.RESOURCE_READ,
53 Permission.RESOURCE_UPDATE,
54 Permission.RESOURCE_TRANSFER,
55 Permission.COHORT_MAINTENANCE,
56 Permission.ARMORY_ACCESS,
57 Permission.ACCESS_ARMORY,
58 Permission.ACCESS_TREASURY,
59 ],
60
61 [Role.CENTURION]: [
62 Permission.USER_READ,
63 Permission.USER_UPDATE,
64 Permission.RESOURCE_READ,
65 Permission.RESOURCE_UPDATE,
66 Permission.RESOURCE_TRANSFER,
67 Permission.COHORT_PATROL,
68 Permission.COHORT_COMMAND,
69 Permission.COHORT_MAINTENANCE,
70 Permission.ARMORY_ACCESS,
71 Permission.ARMORY_MAINTAIN,
72 Permission.ACCESS_ARMORY,
73 Permission.ACCESS_TREASURY,
74 Permission.ACCESS_STRATEGIC_ROOM,
75 ],
76
77 [Role.LEGATUS]: Object.values(Permission), // All permissions
78
79 [Role.ADMIN]: Object.values(Permission), // All permissions
80};
81
82export function hasPermission(userRole: Role, permission: Permission): boolean {
83 return ROLE_PERMISSIONS[userRole]?.includes(permission) || false;
84}
85
86export function getUserPermissions(userRole: Role): Permission[] {
87 return ROLE_PERMISSIONS[userRole] || [];
88}

Permissions Decorator

1// auth/decorators/permissions.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3import { Permission } from '../enums/permissions.enum';
4
5export const PERMISSIONS_KEY = 'permissions';
6export const RequirePermissions = (...permissions: Permission[]) => 
7 SetMetadata(PERMISSIONS_KEY, permissions);
8
9// Aliases for common legion permissions
10export const CanReadResources = () => RequirePermissions(Permission.RESOURCE_READ);
11export const CanManageResources = () => RequirePermissions(
12 Permission.RESOURCE_CREATE,
13 Permission.RESOURCE_UPDATE,
14 Permission.RESOURCE_DELETE
15);
16export const CanAccessArmory = () => RequirePermissions(Permission.ARMORY_ACCESS);
17export const CanCommandCohort = () => RequirePermissions(Permission.COHORT_COMMAND);

Permissions Guard

1// auth/guards/permissions.guard.ts
2import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
3import { Reflector } from '@nestjs/core';
4import { Permission, hasPermission } from '../enums/permissions.enum';
5import { Role } from '../enums/roles.enum';
6import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';
7import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
8
9@Injectable()
10export class PermissionsGuard implements CanActivate {
11 constructor(private reflector: Reflector) {}
12
13 canActivate(context: ExecutionContext): boolean {
14 const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
15 context.getHandler(),
16 context.getClass(),
17 ]);
18 
19 if (isPublic) {
20 return true;
21 }
22
23 const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(PERMISSIONS_KEY, [
24 context.getHandler(),
25 context.getClass(),
26 ]);
27 
28 if (!requiredPermissions || requiredPermissions.length === 0) {
29 return true;
30 }
31
32 const { user } = context.switchToHttp().getRequest();
33 
34 if (!user) {
35 throw new ForbiddenException('No user information available!');
36 }
37
38 // Check if the user has all required permissions
39 const hasAllPermissions = requiredPermissions.every(permission =>
40 hasPermission(user.role as Role, permission)
41 );
42
43 if (!hasAllPermissions) {
44 const missingPermissions = requiredPermissions.filter(permission =>
45 !hasPermission(user.role as Role, permission)
46 );
47 
48 throw new ForbiddenException(
49 `Missing permissions: ${missingPermissions.join(', ')}. Contact the centurion!`
50 );
51 }
52
53 return true;
54 }
55}

Resource-based Authorization

Resource Owner Guard

1// auth/guards/resource-owner.guard.ts
2import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
3import { Reflector } from '@nestjs/core';
4import { Role } from '../enums/roles.enum';
5
6@Injectable()
7export class ResourceOwnerGuard implements CanActivate {
8 constructor(private reflector: Reflector) {}
9
10 canActivate(context: ExecutionContext): boolean {
11 const request = context.switchToHttp().getRequest();
12 const { user, params } = request;
13 
14 if (!user) {
15 throw new ForbiddenException('No user information available!');
16 }
17
18 // Legati and admins have access to everything
19 if (user.role === Role.LEGATUS || user.role === Role.ADMIN) {
20 return true;
21 }
22
23 // Check if the user is trying to access their own resources
24 const resourceUserId = params.userId || params.id;
25 
26 if (resourceUserId && user.id.toString() === resourceUserId.toString()) {
27 return true;
28 }
29
30 throw new ForbiddenException('You can only manage your own resources!');
31 }
32}

Context-aware Authorization

1// auth/guards/context-aware.guard.ts
2import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
3import { ResourceService } from '../../resource/resource.service';
4import { Role } from '../enums/roles.enum';
5
6@Injectable()
7export class ResourceOwnerContextGuard implements CanActivate {
8 constructor(private resourceService: ResourceService) {}
9
10 async canActivate(context: ExecutionContext): Promise<boolean> {
11 const request = context.switchToHttp().getRequest();
12 const { user, params } = request;
13
14 if (!user) {
15 throw new ForbiddenException('No user information available!');
16 }
17
18 // Legati have access to all resources in the legion
19 if (user.role === Role.LEGATUS || user.role === Role.ADMIN) {
20 return true;
21 }
22
23 const resourceId = params.resourceId || params.id;
24
25 if (!resourceId) {
26 return true; // If there is no resource ID, allow to continue
27 }
28
29 try {
30 const resource = await this.resourceService.findById(resourceId);
31
32 if (!resource) {
33 throw new ForbiddenException('Resource not found!');
34 }
35
36 // Check if the user is the resource owner
37 if (resource.ownerId === user.id) {
38 return true;
39 }
40
41 // Check if the user is in the same cohort as the resource
42 if (user.cohortId === resource.cohortId &&
43 [Role.CENTURION, Role.OPTIO].includes(user.role as Role)) {
44 return true;
45 }
46
47 throw new ForbiddenException('You do not have access to this resource!');
48 } catch (error) {
49 throw new ForbiddenException('Error while checking resource permissions');
50 }
51 }
52}

Usage examples in controllers

1// controllers/resource.controller.ts
2import {
3 Controller,
4 Get,
5 Post,
6 Put,
7 Delete,
8 Body,
9 Param,
10 UseGuards
11} from '@nestjs/common';
12import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
13import { RolesGuard } from '../auth/guards/roles.guard';
14import { PermissionsGuard } from '../auth/guards/permissions.guard';
15import { ResourceOwnerContextGuard } from '../auth/guards/context-aware.guard';
16import { Roles, RequireOptio } from '../auth/decorators/roles.decorator';
17import { RequirePermissions, CanReadResources } from '../auth/decorators/permissions.decorator';
18import { CurrentUser } from '../auth/decorators/current-user.decorator';
19import { Role } from '../auth/enums/roles.enum';
20import { Permission } from '../auth/enums/permissions.enum';
21
22@Controller('resources')
23@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard)
24export class ResourceController {
25 constructor(private resourceService: ResourceService) {}
26
27 // All legionaries can browse resources
28 @Get()
29 @CanReadResources()
30 async getAllResources(@CurrentUser() user: any) {
31 return await this.resourceService.findAllForUser(user);
32 }
33
34 // Only optio and above can add resources
35 @Post()
36 @RequireOptio()
37 @RequirePermissions(Permission.RESOURCE_CREATE)
38 async createResource(@Body() createResourceDto: any, @CurrentUser() user: any) {
39 return await this.resourceService.create(createResourceDto, user);
40 }
41
42 // Only the resource owner or higher ranks can edit
43 @Put(':id')
44 @UseGuards(ResourceOwnerContextGuard)
45 @RequirePermissions(Permission.RESOURCE_UPDATE)
46 async updateResource(
47 @Param('id') id: string,
48 @Body() updateResourceDto: any,
49 @CurrentUser() user: any,
50 ) {
51 return await this.resourceService.update(+id, updateResourceDto, user);
52 }
53
54 // Only legatus can delete resources
55 @Delete(':id')
56 @Roles(Role.LEGATUS)
57 @RequirePermissions(Permission.RESOURCE_DELETE)
58 async deleteResource(@Param('id') id: string) {
59 return await this.resourceService.remove(+id);
60 }
61
62 // Resource transfer between cohorts - special permissions
63 @Post(':id/transfer')
64 @UseGuards(ResourceOwnerContextGuard)
65 @RequirePermissions(Permission.RESOURCE_TRANSFER)
66 async transferResource(
67 @Param('id') id: string,
68 @Body('targetUserId') targetUserId: number,
69 @CurrentUser() user: any,
70 ) {
71 return await this.resourceService.transfer(+id, targetUserId, user);
72 }
73}

Global Guards

1// app.module.ts
2import { Module } from '@nestjs/common';
3import { APP_GUARD } from '@nestjs/core';
4import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
5import { RolesGuard } from './auth/guards/roles.guard';
6
7@Module({
8 // ... other imports
9 providers: [
10 // Global guards - will be applied to all endpoints
11 {
12 provide: APP_GUARD,
13 useClass: JwtAuthGuard,
14 },
15 {
16 provide: APP_GUARD,
17 useClass: RolesGuard,
18 },
19 // ... other providers
20 ],
21})
22export class AppModule {}

Policy-based Authorization

1// auth/policies/resource.policy.ts
2import { Injectable } from '@nestjs/common';
3import { Role } from '../enums/roles.enum';
4
5export interface PolicyContext {
6 user: any;
7 resource?: any;
8 action: string;
9}
10
11@Injectable()
12export class ResourcePolicy {
13 canRead(context: PolicyContext): boolean {
14 const { user, resource } = context;
15
16 // Everyone can read basic resource information
17 if (!resource) return true;
18
19 // Owner can read their resources
20 if (resource.ownerId === user.id) return true;
21
22 // Higher ranks can read resources in their cohort
23 if (user.cohortId === resource.cohortId &&
24 [Role.OPTIO, Role.CENTURION, Role.LEGATUS].includes(user.role)) {
25 return true;
26 }
27
28 return false;
29 }
30
31 canUpdate(context: PolicyContext): boolean {
32 const { user, resource } = context;
33
34 // Only the owner or centurion+ can edit
35 if (resource.ownerId === user.id) return true;
36
37 if (user.cohortId === resource.cohortId &&
38 [Role.OPTIO, Role.CENTURION, Role.LEGATUS].includes(user.role)) {
39 return true;
40 }
41
42 return false;
43 }
44
45 canDelete(context: PolicyContext): boolean {
46 const { user, resource } = context;
47
48 // Only legatus can delete legion resources
49 return user.role === Role.LEGATUS && user.cohortId === resource.cohortId;
50 }
51
52 canTransfer(context: PolicyContext): boolean {
53 const { user, resource } = context;
54
55 // Owner can transfer their resources
56 if (resource.ownerId === user.id) return true;
57
58 // Optio can transfer resources in their cohort
59 if (user.cohortId === resource.cohortId &&
60 [Role.OPTIO, Role.CENTURION, Role.LEGATUS].includes(user.role)) {
61 return true;
62 }
63
64 return false;
65 }
66}

Practical exercise

Implement a permissions system for the Empire's military campaigns:

1// Your code here
2// 1. Define roles for the campaign system (Tiro, Miles, Decanus, Optio, Centurion, Legatus)
3// 2. Create permissions for campaign management (planning, approving, canceling)
4// 3. Implement a context-aware guard checking cohort membership
5// 4. Create a policy for different actions on campaigns
6// 5. Add decorators for convenient use in controllers

Summary

Well done! You have been appointed chief legion prefect! Now you can:

Implement RBAC with Roman army rank hierarchy ✓ Manage permissions at the individual action level ✓ Create Guards for different authorization scenarios ✓ Implement resource-based and context-aware authorization ✓ Use decorators for readable and maintainable code ✓ Design policy-based systems for complex business rules

Now every legionary knows their place in the hierarchy and has access only to what their rank entitles them to!

Go to CodeWorlds