Skip to content

Commit

Permalink
Build Roles Authorization
Browse files Browse the repository at this point in the history
  • Loading branch information
zenkiet committed Sep 23, 2023
1 parent 524dac1 commit 4ea237a
Show file tree
Hide file tree
Showing 17 changed files with 116 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ CREATE TABLE "Role" (
"role_id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"userId" INTEGER,

CONSTRAINT "Role_pkey" PRIMARY KEY ("role_id")
);
Expand Down Expand Up @@ -113,6 +112,12 @@ CREATE TABLE "Activity" (
CONSTRAINT "Activity_pkey" PRIMARY KEY ("activity_id")
);

-- CreateTable
CREATE TABLE "_RoleToUser" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);

-- CreateTable
CREATE TABLE "_PermissionToRole" (
"A" INTEGER NOT NULL,
Expand Down Expand Up @@ -152,6 +157,12 @@ CREATE INDEX "user_email_idx" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Label_name_key" ON "Label"("name");

-- CreateIndex
CREATE UNIQUE INDEX "_RoleToUser_AB_unique" ON "_RoleToUser"("A", "B");

-- CreateIndex
CREATE INDEX "_RoleToUser_B_index" ON "_RoleToUser"("B");

-- CreateIndex
CREATE UNIQUE INDEX "_PermissionToRole_AB_unique" ON "_PermissionToRole"("A", "B");

Expand All @@ -170,9 +181,6 @@ CREATE UNIQUE INDEX "_LabelToTask_AB_unique" ON "_LabelToTask"("A", "B");
-- CreateIndex
CREATE INDEX "_LabelToTask_B_index" ON "_LabelToTask"("B");

-- AddForeignKey
ALTER TABLE "Role" ADD CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Condition" ADD CONSTRAINT "Condition_policyId_fkey" FOREIGN KEY ("policyId") REFERENCES "Policy"("policy_id") ON DELETE CASCADE ON UPDATE CASCADE;

Expand All @@ -188,6 +196,12 @@ ALTER TABLE "Activity" ADD CONSTRAINT "Activity_taskId_fkey" FOREIGN KEY ("taskI
-- AddForeignKey
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("role_id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_PermissionToRole" ADD CONSTRAINT "_PermissionToRole_A_fkey" FOREIGN KEY ("A") REFERENCES "Permission"("permission_id") ON DELETE CASCADE ON UPDATE CASCADE;

Expand Down
3 changes: 1 addition & 2 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ model Role {
description String?
permissions Permission[]
policies Policy[]
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
User User[]
}

model Permission {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ describe('SessionAuthenticationController', () => {
controllers: [SessionAuthenticationController],
}).compile();

controller = module.get<SessionAuthenticationController>(SessionAuthenticationController);
controller = module.get<SessionAuthenticationController>(
SessionAuthenticationController,
);
});

it('should be defined', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class AccessTokenGuard extends AuthGuard('jwt') implements CanActivate {

handleRequest(err: any, user: any, info: any) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
if (err || !user || info) {
throw err || new UnauthorizedException('No access token found');
}
return user;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface ActiveUserData {
sub: number;
email: string;
roles?: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class AuthenticationService {

const payload = {
email: user.email,
roles: user.roles.map((role) => role.name),
};

const [accessToken, refreshToken] = await Promise.all([
Expand Down Expand Up @@ -133,7 +134,7 @@ export class AuthenticationService {
throw new UnauthorizedException('Invalid refresh token');
}

return await this.generateToken(user);
return await this.generateToken(user as UserEntity);
} catch (error) {
if (error instanceof RefreshTokenIdsStorageError) {
throw new UnauthorizedException('Invalid refresh token');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ describe('SessionAuthenticationService', () => {
providers: [SessionAuthenticationService],
}).compile();

service = module.get<SessionAuthenticationService>(SessionAuthenticationService);
service = module.get<SessionAuthenticationService>(
SessionAuthenticationService,
);
});

it('should be defined', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { PassportSerializer } from '@nestjs/passport';
import { User } from '@prisma/client';
import { UserEntity } from 'src/users/entity/user.entity';
import passport from 'passport';
import { ActiveUserData } from '../../interfaces/active-user-data.interface';

export class UserSerializer implements PassportSerializer {
constructor() {
const passportInstance = this.getPassportInstance();
passportInstance.serializeUser((user, done) =>
this.serializeUser(user as User, done),
this.serializeUser(user as UserEntity, done),
);
passportInstance.deserializeUser((payload, done) =>
this.deserializeUser(payload as ActiveUserData, done),
Expand All @@ -18,12 +18,15 @@ export class UserSerializer implements PassportSerializer {
return passport;
}

serializeUser(user: User, done: (err: Error, user: ActiveUserData) => void) {
serializeUser(
user: UserEntity,
done: (err: Error, user: ActiveUserData) => void,
) {
// store user info authenticated in session
done(null, {
sub: user.id,
email: user.email,
// role: user.role,
role: user.roles,
// permissions: user.permissions as any,
});
}
Expand Down
13 changes: 13 additions & 0 deletions server/src/iam/authorization/authorization.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './guards/roles/roles.guard';

@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AuthorizationModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { RoleEntity } from 'src/users/entity/role.entity';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: RoleEntity[]) => SetMetadata(ROLES_KEY, roles);
7 changes: 7 additions & 0 deletions server/src/iam/authorization/guards/roles/roles.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RolesGuard } from './roles.guard';

describe('RolesGuard', () => {
it('should be defined', () => {
expect(new RolesGuard()).toBeDefined();
});
});
30 changes: 30 additions & 0 deletions server/src/iam/authorization/guards/roles/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { RoleEntity } from 'src/users/entity/role.entity';
import { ActiveUserData } from 'src/iam/authentication/interfaces/active-user-data.interface';
import { REQUEST_USER_KEY } from '../../../constants/iam.contant';
import { ROLES_KEY } from '../../decorators/roles/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const contextRole = this.reflector.getAllAndOverride<RoleEntity[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);

if (contextRole) {
const user: ActiveUserData = context.switchToHttp().getRequest()[
REQUEST_USER_KEY
];

return contextRole.some((role) => user.roles?.includes(role.name));
}

return true;
}
}
3 changes: 2 additions & 1 deletion server/src/iam/iam.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { AuthenticationModule } from './authentication/authentication.module';
import { AuthorizationModule } from './authorization/authorization.module';

@Module({
providers: [],
controllers: [],
imports: [AuthenticationModule],
imports: [AuthenticationModule, AuthorizationModule],
})
export class IamModule {}
11 changes: 11 additions & 0 deletions server/src/users/entity/role.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Role } from '@prisma/client';

export class RoleEntity implements Role {
name: string;

constructor(name: string) {
this.name = name;
}
id: number;
description: string;
}
4 changes: 4 additions & 0 deletions server/src/users/entity/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { User } from '@prisma/client';
import { ApiProperty } from '@nestjs/swagger';
import { IsDate, IsOptional } from 'class-validator';
import { RoleEntity } from './role.entity';

export class UserEntity implements User {
@ApiProperty()
Expand All @@ -25,6 +26,9 @@ export class UserEntity implements User {
@IsOptional()
tfaSecret: string | null;

@ApiProperty()
roles: RoleEntity[];

@IsDate()
createdAt: Date;

Expand Down
4 changes: 4 additions & 0 deletions server/src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { UsersService } from './users.service';
import { Prisma } from '@prisma/client';
import { CreateUserDto } from './dto/create-user.dto';
import { updateUserDto } from './dto/update-user.dto';
import { Roles } from '../iam/authorization/decorators/roles/roles.decorator';
import { RoleEntity } from './entity/role.entity';

@Controller('users')
export class UsersController {
Expand All @@ -21,6 +23,8 @@ export class UsersController {
return this.usersService.create(createUserDto);
}

@Get()
@Roles(new RoleEntity('ADMIN'))
@Get()
async findAll() {
return this.usersService.findAll();
Expand Down
5 changes: 4 additions & 1 deletion server/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ export class UsersService {
});
}

async findOne(where: Prisma.UserWhereUniqueInput): Promise<User | null> {
async findOne(where: Prisma.UserWhereUniqueInput) {
return this.prismaService.user.findUnique({
where,
include: {
roles: true,
},
});
}

Expand Down

0 comments on commit 4ea237a

Please sign in to comment.