Skip to content

Commit

Permalink
feat: wip - adding rbac
Browse files Browse the repository at this point in the history
  • Loading branch information
crazyoptimist committed May 31, 2024
1 parent 978f5db commit d7a55f9
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 4 deletions.
43 changes: 43 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"prepare": "husky install"
},
"dependencies": {
"@casl/ability": "^6.7.1",
"@nestjs/common": "^10.3.8",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.3.8",
Expand Down
7 changes: 7 additions & 0 deletions src/modules/infrastructure/casl/action.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum Action {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
30 changes: 30 additions & 0 deletions src/modules/infrastructure/casl/casl-ability.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
AbilityBuilder,
ExtractSubjectType,
InferSubjects,
PureAbility,
AbilityClass,
} from '@casl/ability';
import { Injectable } from '@nestjs/common';
import { Action } from './action.enum';
import { User } from '@app/modules/user/user.entity';

type Subjects = InferSubjects<typeof User | User> | 'all';

export type AppAbility = PureAbility<[Action, Subjects]>;

@Injectable()
export class CaslAbilityFactory {
createForUser(user: User) {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
PureAbility as AbilityClass<AppAbility>,
);

// TODO: Build abilities

return build({
detectSubjectType: (subject) =>
subject.constructor as ExtractSubjectType<Subjects>,
});
}
}
9 changes: 9 additions & 0 deletions src/modules/infrastructure/casl/casl.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { CaslAbilityFactory } from './casl-ability.factory';

@Global()
@Module({
providers: [CaslAbilityFactory],
exports: [CaslAbilityFactory],
})
export class CaslModule {}
6 changes: 6 additions & 0 deletions src/modules/infrastructure/casl/check-policies.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
import { PolicyHandler } from './policies.guard';

export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
SetMetadata(CHECK_POLICIES_KEY, handlers);
42 changes: 42 additions & 0 deletions src/modules/infrastructure/casl/policies.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AppAbility, CaslAbilityFactory } from './casl-ability.factory';
import { CHECK_POLICIES_KEY } from './check-policies.decorator';

interface IPolicyHandler {
handle(ability: AppAbility): boolean;
}

type PolicyHandlerCallback = (ability: AppAbility) => boolean;

export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;

@Injectable()
export class PoliciesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private caslAbilityFactory: CaslAbilityFactory,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const policyHandlers =
this.reflector.get<PolicyHandler[]>(
CHECK_POLICIES_KEY,
context.getHandler(),
) || [];

const { user } = context.switchToHttp().getRequest();
const ability = this.caslAbilityFactory.createForUser(user);

return policyHandlers.every((handler) =>
this.execPolicyHandler(handler, ability),
);
}

private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
if (typeof handler === 'function') {
return handler(ability);
}
return handler.handle(ability);
}
}
18 changes: 18 additions & 0 deletions src/modules/user/role.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity({
name: 'roles',
})
export class Role {
@PrimaryGeneratedColumn()
id: number;

@Column({ length: 50 })
name: string;
}

// We treat roles as schema
export enum RoleEnum {
Admin = 'admin',
User = 'user',
}
21 changes: 18 additions & 3 deletions src/modules/user/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
ManyToMany,
JoinTable,
} from 'typeorm';
import { Exclude } from 'class-transformer';

import { PasswordTransformer } from '../common/transformer/password.transformer';
import { Role } from './role.entity';

@Entity({
name: 'users',
})
export class User {
@PrimaryGeneratedColumn()
id: number;

@CreateDateColumn()
@Exclude()
created_at: Date;
Expand All @@ -21,9 +27,6 @@ export class User {
@Exclude()
updated_at: Date;

@PrimaryGeneratedColumn()
id: number;

@Column({ name: 'first_name', length: 255 })
firstName: string;

Expand All @@ -40,4 +43,16 @@ export class User {
})
@Exclude()
password: string;

@ManyToMany(() => Role, { eager: true })
@JoinTable({
name: 'users_roles',
joinColumn: {
name: 'user_id',
},
inverseJoinColumn: {
name: 'role_id',
},
})
roles: Role[];
}
3 changes: 2 additions & 1 deletion src/modules/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { Role } from './role.entity';

@Module({
imports: [TypeOrmModule.forFeature([User])],
imports: [TypeOrmModule.forFeature([User, Role])],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
Expand Down

0 comments on commit d7a55f9

Please sign in to comment.