Skip to content

Commit

Permalink
basic RBAC system
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 9, 2024
1 parent 42fc944 commit 68a4948
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 2 deletions.
7 changes: 6 additions & 1 deletion api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { ApiConfigModule } from '@api/modules/config/app-config.module';
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from '@api/modules/auth/auth.module';
import { JwtAuthGuard } from '@api/modules/auth/guards/jwt-auth.guard';
import { RolesGuard } from '@api/modules/auth/guards/roles.guard';

@Module({
imports: [ApiConfigModule, AuthModule],
controllers: [AppController],
providers: [AppService, { provide: APP_GUARD, useClass: JwtAuthGuard }],
providers: [
AppService,
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class AppModule {}
11 changes: 11 additions & 0 deletions api/src/modules/auth/authorisation/roles.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum ROLES {
ADMIN = 'admin',
PARTNER = 'partner',
GENERAL_USER = 'general_user',
}

export const ROLES_HIERARCHY = {
[ROLES.ADMIN]: [ROLES.PARTNER, ROLES.GENERAL_USER],
[ROLES.PARTNER]: [ROLES.GENERAL_USER],
[ROLES.GENERAL_USER]: [],
};
6 changes: 6 additions & 0 deletions api/src/modules/auth/decorators/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
import { ROLES } from '@api/modules/auth/authorisation/roles.enum';

export const ROLES_KEY = 'roles';
export const RequiredRoles = (...roles: ROLES[]) =>
SetMetadata(ROLES_KEY, roles);
33 changes: 33 additions & 0 deletions api/src/modules/auth/guards/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
ROLES,
ROLES_HIERARCHY,
} from '@api/modules/auth/authorisation/roles.enum';
import { ROLES_KEY } from '@api/modules/auth/decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const requiredRoles: ROLES[] = this.reflector.getAllAndOverride<ROLES[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();

return this.hasRequiredRole(user.role, requiredRoles);
}

private hasRequiredRole(userRole: ROLES, requiredRoles: ROLES[]): boolean {
return requiredRoles.some(
(requiredRole) =>
userRole === requiredRole ||
ROLES_HIERARCHY[userRole]?.includes(requiredRole),
);
}
}
26 changes: 26 additions & 0 deletions api/src/modules/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller, Get } from '@nestjs/common';
import { RequiredRoles } from '@api/modules/auth/decorators/roles.decorator';
import { ROLES } from '@api/modules/auth/authorisation/roles.enum';

@Controller('users')
export class UsersController {
// TODO: All of these endpoints are fake, only to test the role guard

@RequiredRoles(ROLES.ADMIN)
@Get('admin')
async createUserAsAdmin() {
return [ROLES.ADMIN];
}

@RequiredRoles(ROLES.PARTNER)
@Get('partner')
async createUserAsPartner() {
return [ROLES.PARTNER, ROLES.ADMIN];
}

@RequiredRoles(ROLES.GENERAL_USER)
@Get('user')
async createUserAsUser() {
return [ROLES.GENERAL_USER, ROLES.PARTNER, ROLES.ADMIN];
}
}
2 changes: 2 additions & 0 deletions api/src/modules/users/users.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '@shared/entities/users/user.entity';
import { UsersController } from '@api/modules/users/users.controller';

@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
File renamed without changes.
42 changes: 42 additions & 0 deletions api/test/auth/authorization.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { TestManager } from '../utils/test-manager';

import { User } from '@shared/entities/users/user.entity';
import { ROLES } from '@api/modules/auth/authorisation/roles.enum';

describe('Authorization', () => {
let testManager: TestManager;

beforeAll(async () => {
testManager = await TestManager.createTestManager();
});

afterEach(async () => {
await testManager.clearDatabase();
});

afterAll(async () => {
await testManager.close();
});
test('a user should have a default general user role when signing up', async () => {
await testManager
.request()
.post('/authentication/signup')
.send({ email: '[email protected]', password: '123456' });

const user = await testManager
.getDataSource()
.getRepository(User)
.findOne({ where: { email: '[email protected]' } });

expect(user.role).toEqual(ROLES.GENERAL_USER);

describe('ROLE TEST ENDPOINTS, REMOVE!', () => {
test('when role required is general user, the general user and above roles should have access', async () => {
const user = await testManager
.mocks()
.createUser({ role: ROLES.GENERAL_USER });
const { jwtToken } = await testManager.logUserIn(user);
});
});
});
});
2 changes: 1 addition & 1 deletion api/test/utils/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class TestManager {

mocks() {
return {
createUser: (additionalData: Partial<User>) =>
createUser: (additionalData?: Partial<User>) =>
createUser(this.getDataSource(), additionalData),
};
}
Expand Down
9 changes: 9 additions & 0 deletions shared/entities/users/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PrimaryGeneratedColumn,
} from "typeorm";
import { Exclude } from "class-transformer";
import { ROLES } from "@api/modules/auth/authorisation/roles.enum";

@Entity({ name: "users" })
export class User {
Expand All @@ -19,6 +20,14 @@ export class User {
@Exclude()
password: string;

@Column({
type: "enum",
default: ROLES.GENERAL_USER,
enum: ROLES,
enumName: "user_roles",
})
role: ROLES;

@CreateDateColumn({ name: "created_at" })
createdAt: Date;
}

0 comments on commit 68a4948

Please sign in to comment.