Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Role-based Authorization - hierarchia w Imperium

Organizatorze hierarchii! Architekt Vitruvius zauważył, że choć wszyscy legioniści mają już dostęp do obozu, to nie wszyscy powinni mieć dostęp do wszystkich pomieszczeń. Czas nauczyć się Role-based Authorization - systemu, który zapewnia, że każdy legionariusz ma dostęp tylko do tego, na co zasługuje jego ranga w armii rzymskiej!

Czym jest Authorization?

Wyobraź sobie, że jesteś prefektem odpowiedzialnym za porządek w obozie legionowym. Każdy legionariusz już przeszedł przez bramę (authentication), ale teraz musisz zdecydować, do których części obozu ma dostęp:

  • Tiro (rekrut) - może sprzątać i pomagać w kuchni polowej
  • Miles (żołnierz) - może pełnić warty i wykonywać podstawowe zadania
  • Decanus (dowódca dziesiątki) - ma dostęp do zbrojowni i magazynów
  • Optio (zastępca centuriona) - zarządza zaopatrzeniem i szkoleniem
  • Centurion (dowódca centurii) - może kierować oddziałem i planować kampanie
  • Legatus (dowódca legionu) - ma dostęp do wszystkiego w całym legionie

Authorization to proces określania, do czego uwierzytelniony użytkownik ma dostęp.

Role-based Access Control (RBAC)

Definicja ról

1// enums/roles.enum.ts
2export enum Role {
3 TIRO = 'TIRO',             // Rekrut
4 MILES = 'MILES',           // Żołnierz
5 DECANUS = 'DECANUS',       // Dowódca dziesiątki
6 OPTIO = 'OPTIO',           // Zastępca centuriona
7 CENTURION = 'CENTURION',   // Dowódca centurii
8 LEGATUS = 'LEGATUS',       // Dowódca legionu
9 ADMIN = 'ADMIN',           // Super admin Imperium
10}
11
12// Hierarchia ról (wyższe rangi dziedziczą uprawnienia niższych)
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// Aliasy dla wygody - rangi armii rzymskiej
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 // Sprawdź czy endpoint jest publiczny
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 // Pobierz wymagane role
24 const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
25 context.getHandler(),
26 context.getClass(),
27 ]);
28 
29 if (!requiredRoles) {
30 return true; // Brak ograniczeń ról
31 }
32
33 const { user } = context.switchToHttp().getRequest();
34 
35 if (!user) {
36 throw new ForbiddenException('Brak informacji o użytkowniku!');
37 }
38
39 // Sprawdź czy użytkownik ma jedną z wymaganych ról
40 const hasRole = requiredRoles.some(role => 
41 hasPermission(user.role as Role, role)
42 );
43
44 if (!hasRole) {
45 throw new ForbiddenException(
46 `Brak uprawnień! Wymagana rola: ${requiredRoles.join(' lub ')}. Twoja rola: ${user.role}`
47 );
48 }
49
50 return true;
51 }
52}

Permission-based Authorization

Definicja uprawnień

1// enums/permissions.enum.ts
2export enum Permission {
3 // Zarządzanie użytkownikami
4 USER_CREATE = 'user:create',
5 USER_READ = 'user:read',
6 USER_UPDATE = 'user:update',
7 USER_DELETE = 'user:delete',
8 
9 // Zarządzanie zasobami legionu
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 // Zarządzanie kohortą
17 COHORT_PATROL = 'cohort:patrol',
18 COHORT_COMMAND = 'cohort:command',
19 COHORT_MAINTENANCE = 'cohort:maintenance',
20
21 // Zarządzanie uzbrojeniem
22 ARMORY_ACCESS = 'armory:access',
23 ARMORY_MAINTAIN = 'armory:maintain',
24
25 // Specjalne uprawnienia
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// Mapowanie ról do uprawnień - hierarchia armii rzymskiej
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), // Wszystkie uprawnienia
78
79 [Role.ADMIN]: Object.values(Permission), // Wszystkie uprawnienia
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// Aliasy dla częstych uprawnień legionu
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('Brak informacji o użytkowniku!');
36 }
37
38 // Sprawdź czy użytkownik ma wszystkie wymagane uprawnienia
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 `Brak uprawnień: ${missingPermissions.join(', ')}. Skontaktuj się z centurionem!`
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('Brak informacji o użytkowniku!');
16 }
17
18 // Legatowie i adminowie mają dostęp do wszystkiego
19 if (user.role === Role.LEGATUS || user.role === Role.ADMIN) {
20 return true;
21 }
22
23 // Sprawdź czy użytkownik próbuje uzyskać dostęp do własnych zasobów
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('Możesz zarządzać tylko własnymi zasobami!');
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('Brak informacji o użytkowniku!');
16 }
17
18 // Legatowie mają dostęp do wszystkich zasobów w legionie
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; // Jeśli nie ma ID zasobu, pozwól dalej
27 }
28
29 try {
30 const resource = await this.resourceService.findById(resourceId);
31
32 if (!resource) {
33 throw new ForbiddenException('Zasób nie został znaleziony!');
34 }
35
36 // Sprawdź czy użytkownik jest właścicielem zasobu
37 if (resource.ownerId === user.id) {
38 return true;
39 }
40
41 // Sprawdź czy użytkownik jest w tej samej kohorcie co zasób
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('Nie masz dostępu do tego zasobu!');
48 } catch (error) {
49 throw new ForbiddenException('Błąd podczas sprawdzania uprawnień do zasobu');
50 }
51 }
52}

Przykłady użycia w kontrolerach

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 // Wszyscy legioniści mogą przeglądać zasoby
28 @Get()
29 @CanReadResources()
30 async getAllResources(@CurrentUser() user: any) {
31 return await this.resourceService.findAllForUser(user);
32 }
33
34 // Tylko optio i wyżej mogą dodawać zasoby
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 // Tylko właściciel zasobu lub wyższe rangi mogą edytować
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 // Tylko legatus może usuwać zasoby
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 // Transfer zasobów między kohortami - specjalne uprawnienia
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 // ... inne imports
9 providers: [
10 // Globalne guardy - będą stosowane do wszystkich endpointów
11 {
12 provide: APP_GUARD,
13 useClass: JwtAuthGuard,
14 },
15 {
16 provide: APP_GUARD,
17 useClass: RolesGuard,
18 },
19 // ... inne 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 // Wszyscy mogą czytać podstawowe informacje o zasobach
17 if (!resource) return true;
18
19 // Właściciel może czytać swoje zasoby
20 if (resource.ownerId === user.id) return true;
21
22 // Wyższe rangi mogą czytać zasoby w swojej kohorcie
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 // Tylko właściciel lub centurion+ mogą edytować
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 // Tylko legatus może usuwać zasoby legionu
49 return user.role === Role.LEGATUS && user.cohortId === resource.cohortId;
50 }
51
52 canTransfer(context: PolicyContext): boolean {
53 const { user, resource } = context;
54
55 // Właściciel może transferować swoje zasoby
56 if (resource.ownerId === user.id) return true;
57
58 // Optio może transferować zasoby w swojej kohorcie
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}

Praktyczne ćwiczenie

Zaimplementuj system uprawnień dla kampanii militarnych Imperium:

1// Twój kod tutaj
2// 1. Zdefiniuj role dla systemu kampanii (Tiro, Miles, Decanus, Optio, Centurion, Legatus)
3// 2. Stwórz uprawnienia dla zarządzania kampaniami (planowanie, zatwierdzanie, anulowanie)
4// 3. Zaimplementuj context-aware guard sprawdzający przynależność do kohorty
5// 4. Stwórz policy dla różnych akcji na kampaniach
6// 5. Dodaj dekoratory dla wygodnego użycia w kontrolerach

Podsumowanie

Brawo! Zostałeś mianowany głównym prefektem legionu! Teraz umiesz:

Implementować RBAC z hierarchią rang armii rzymskiej ✓ Zarządzać uprawnieniami na poziomie poszczególnych akcji ✓ Tworzyć Guards dla różnych scenariuszy autoryzacji ✓ Implementować resource-based i context-aware authorization ✓ Używać dekoratorów dla czytelnego i utrzymywalnego kodu ✓ Projektować policy-based systems dla złożonych reguł biznesowych

Teraz każdy legionariusz zna swoje miejsce w hierarchii i ma dostęp tylko do tego, na co zasługuje jego ranga!

Przejdź do CodeWorlds