Skip to content

Commit

Permalink
implement Role based access control
Browse files Browse the repository at this point in the history
  • Loading branch information
olasunkanmi-SE committed Dec 5, 2023
1 parent a93626c commit c64efc5
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 2 deletions.
14 changes: 14 additions & 0 deletions backend/src/application/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ export enum MerchantStatus {
}

export const tokenExpiresIn = 3600000;

export enum Role {
ADMIN = 'ADMIN',
USER = 'USER',
GUEST = 'CLIENT',
}

export const RoleOrder: Record<Role, number> = {
[Role.GUEST]: 1,
[Role.USER]: 2,
[Role.ADMIN]: 3,
};

export const ROLE_KEY = 'role';
2 changes: 2 additions & 0 deletions backend/src/application/constants/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ export const TYPES = {
IOrderProcessingQueueRepository: Symbol('IOrderProcessingQueueRepository'),
IOrderProcessingQueueService: Symbol('IOrderProcessingQueueService'),
IMerchantRepository: Symbol('IMerchantRepository'),
IRoleService: Symbol('IRoleService'),
IAccessControlService: Symbol('IAccessControlService'),
};
1 change: 1 addition & 0 deletions backend/src/infrastructure/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './get-user-id.decorator';
export * from './get-user.decorator';
export * from './roles.decorators';
4 changes: 4 additions & 0 deletions backend/src/infrastructure/decorators/roles.decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
import { ROLE_KEY, Role } from 'src/application';

export const Roles = (...role: Role[]) => SetMetadata(ROLE_KEY, role);
35 changes: 35 additions & 0 deletions backend/src/infrastructure/guards/role-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { ROLE_KEY, Role, TYPES } from 'src/application';
import { IAccessControlService } from 'src/shared/interfaces/access_control_service.interface';
import { Context, IContextService } from '../context';

@Injectable()
export class RoleGuard implements CanActivate {
private context: Context;
constructor(
private readonly reflector: Reflector,
@Inject(TYPES.IAccessControlService) private readonly accessControlService: IAccessControlService,
@Inject(TYPES.IContextService)
private readonly contextService: IContextService,
) {
this.context = this.contextService.getContext();
}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLE_KEY, [
context.getHandler(),
context.getClass(),
]);
for (const role of requiredRoles) {
const isAuthorized = this.accessControlService.isAuthorized({
currentRole: this.context.role as Role,
requiredRole: role,
});
if (isAuthorized) {
return true;
}
}
return false;
}
}
6 changes: 5 additions & 1 deletion backend/src/merchant/merchant.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { CreateMerchantDTO } from './dtos/create-merchant.dto';
import { OnBoardMerchantDTO } from './dtos/on-board-merchant.dto';
import { IMerchantService } from './interface/merchant-service.interface';
import { IMerchantResponseDTO } from './merchant-response.dto';
import { RoleGuard } from 'src/infrastructure/guards/role-guard';
import { Roles } from 'src/infrastructure';
import { Role } from 'src/application';

@Controller('merchants')
export class MerchantController {
Expand All @@ -29,7 +32,8 @@ export class MerchantController {
return this.merchantService.getMerchantById(merchantId);
}

@UseGuards(AccessAuthGuard)
@UseGuards(AccessAuthGuard, RoleGuard)
@Roles(Role.ADMIN)
@Get()
@HttpCode(HttpStatus.OK)
async getMerchants(): Promise<Result<IMerchantResponseDTO[]>> {
Expand Down
6 changes: 5 additions & 1 deletion backend/src/merchant/merchant.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { AccessControlService } from 'src/shared/services/access_control.service';
import { RoleService } from 'src/shared/services/role_service';
import { MerchantRepository } from '../infrastructure/data_access/repositories/merchant.repository';
import { TYPES } from './../application/constants/types';
import { AuditMapper } from './../audit/audit.mapper';
import { AuthModule } from './../infrastructure/auth/auth.module';
import { AuthService } from './../infrastructure/auth/auth.service';
import { ContextService } from './../infrastructure/context/context.service';
import { MerchantRepository } from '../infrastructure/data_access/repositories/merchant.repository';
import {
MerchantDataModel,
MerchantSchema,
Expand All @@ -30,6 +32,8 @@ import { MerchantService } from './merchant.service';
{ provide: TYPES.IContextService, useClass: ContextService },
{ provide: TYPES.IValidateUser, useClass: ValidateUser },
{ provide: TYPES.IMapper, useClass: MerchantMapper },
{ provide: TYPES.IAccessControlService, useClass: AccessControlService },
{ provide: TYPES.IRoleService, useClass: RoleService },
],
controllers: [MerchantController],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { IIsAuthorizedProps } from './shared.interface';

export interface IAccessControlService {
isAuthorized({ currentRole, requiredRole }: IIsAuthorizedProps): boolean;
}
5 changes: 5 additions & 0 deletions backend/src/shared/interfaces/role_service.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Role } from '../../application';

export interface IRoleService {
sortRoles(roles: Role[]): Role[];
}
6 changes: 6 additions & 0 deletions backend/src/shared/interfaces/shared.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Role } from 'src/application';

export interface IIsAuthorizedProps {
currentRole: Role;
requiredRole: Role;
}
39 changes: 39 additions & 0 deletions backend/src/shared/services/access_control.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Inject, Injectable } from '@nestjs/common';
import { Role, TYPES } from 'src/application';
import { IAccessControlService } from '../interfaces/access_control_service.interface';
import { IRoleService } from '../interfaces/role_service.interface';
import { IIsAuthorizedProps } from '../interfaces/shared.interface';

@Injectable()
export class AccessControlService implements IAccessControlService {
private hierarchies: Map<string, number>[] = [];
private priority = 1;

constructor(@Inject(TYPES.IRoleService) private readonly roleService: IRoleService) {
this.mapRoleToPriority();
}

private mapRoleToPriority(): void {
const sortedRoles = this.roleService.sortRoles(Object.values(Role));
if (sortedRoles?.length) {
sortedRoles.reduce((map, role) => {
map.set(role, this.priority);
this.priority++;
this.hierarchies.push(map);
return map;
}, new Map<string, number>());
}
}

public isAuthorized({ currentRole, requiredRole }: IIsAuthorizedProps): boolean {
let authorized = false;
for (const hierarchy of this.hierarchies) {
const priority = hierarchy.get(currentRole);
const requirePriority = hierarchy.get(requiredRole);
if (priority && requirePriority && priority >= requirePriority) {
authorized = true;
}
}
return authorized;
}
}
14 changes: 14 additions & 0 deletions backend/src/shared/services/role_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { Role, RoleOrder } from '../../application';
import { IRoleService } from '../interfaces/role_service.interface';

@Injectable()
export class RoleService implements IRoleService {
sortRoles(roles: Role[]): Role[] {
const validRoles = roles.filter((role) => role in RoleOrder);
if (validRoles?.length) {
validRoles.sort((a, b) => RoleOrder[a] - RoleOrder[b]);
}
return validRoles;
}
}

0 comments on commit c64efc5

Please sign in to comment.