From 6802cd362018d2acd92d1771bece5f2aa5ada364 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 15:03:58 +0100 Subject: [PATCH 01/13] add opa, refactor auth to authentication --- .env.dist | 3 + .env.test | 3 + docker-compose.yml | 19 ++++ opa/user/user1.rego | 7 ++ opa/user/user2.rego | 7 ++ src/app.module.ts | 4 +- src/main.ts | 4 +- .../adapters/secondaries/auth.repository.ts | 8 -- .../auth/commands/create-auth.command.ts | 9 -- .../auth/commands/delete-auth.command.ts | 9 -- .../tests/unit/create-auth.usecase.spec.ts | 91 --------------- .../tests/unit/validate-auth.usecase.spec.ts | 104 ----------------- .../authentication-messager.controller.ts} | 12 +- .../primaries/authentication.controller.ts} | 50 ++++---- .../primaries/authentication.presenter.ts} | 2 +- .../adapters/primaries/authentication.proto} | 12 +- .../adapters/primaries/rpc.validation-pipe.ts | 0 .../adapters/primaries/username.presenter.ts | 0 .../secondaries/authentication.messager.ts} | 2 +- .../secondaries/authentication.repository.ts | 8 ++ .../adapters/secondaries/logging.messager.ts | 0 .../secondaries/username.repository.ts | 4 +- .../authentication.module.ts} | 28 ++--- .../commands/add-username.command.ts | 0 .../commands/create-authentication.command.ts | 9 ++ .../commands/delete-authentication.command.ts | 9 ++ .../commands/delete-username.command.ts | 0 .../commands/update-password.command.ts | 0 .../commands/update-username.command.ts | 0 .../domain/dtos/add-username.request.ts | 0 .../dtos/create-authentication.request.ts} | 2 +- .../dtos/delete-authentication.request.ts} | 2 +- .../domain/dtos/delete-username.request.ts | 0 .../domain/dtos/type.enum.ts | 0 .../domain/dtos/update-password.request.ts | 0 .../domain/dtos/update-username.request.ts | 0 .../dtos/validate-authentication.request.ts} | 2 +- .../domain/entities/authentication.ts} | 2 +- .../domain/entities/username.ts | 0 .../domain/interfaces/message-broker.ts | 0 .../domain/usecases/add-username.usecase.ts | 0 .../create-authentication.usecase.ts} | 18 +-- .../delete-authentication.usecase.ts} | 18 +-- .../usecases/delete-username.usecase.ts | 0 .../usecases/update-password.usecase.ts | 10 +- .../usecases/update-username.usecase.ts | 0 .../validate-authentication.usecase.ts} | 18 +-- .../mappers/authentication.profile.ts} | 8 +- .../mappers/username.profile.ts | 0 .../queries/validate-authentication.query.ts} | 2 +- .../authentication.repository.spec.ts} | 71 +++++++----- .../integration/username.repository.spec.ts | 0 .../tests/unit/add-username.usecase.spec.ts | 4 +- .../create-authentication.usecase.spec.ts | 99 ++++++++++++++++ .../delete-authentication.usecase.spec.ts} | 48 ++++---- .../unit/delete-username.usecase.spec.ts | 0 .../unit/update-password.usecase.spec.ts | 12 +- .../unit/update-username.usecase.spec.ts | 0 .../validate-authentication.usecase.spec.ts | 107 ++++++++++++++++++ src/modules/database/database.module.ts | 8 +- ...uthnz-repository.ts => auth-repository.ts} | 2 +- 61 files changed, 456 insertions(+), 381 deletions(-) create mode 100644 opa/user/user1.rego create mode 100644 opa/user/user2.rego delete mode 100644 src/modules/auth/adapters/secondaries/auth.repository.ts delete mode 100644 src/modules/auth/commands/create-auth.command.ts delete mode 100644 src/modules/auth/commands/delete-auth.command.ts delete mode 100644 src/modules/auth/tests/unit/create-auth.usecase.spec.ts delete mode 100644 src/modules/auth/tests/unit/validate-auth.usecase.spec.ts rename src/modules/{auth/adapters/primaries/auth-messager.controller.ts => authentication/adapters/primaries/authentication-messager.controller.ts} (82%) rename src/modules/{auth/adapters/primaries/auth.controller.ts => authentication/adapters/primaries/authentication.controller.ts} (71%) rename src/modules/{auth/adapters/primaries/auth.presenter.ts => authentication/adapters/primaries/authentication.presenter.ts} (66%) rename src/modules/{auth/adapters/primaries/auth.proto => authentication/adapters/primaries/authentication.proto} (72%) rename src/modules/{auth => authentication}/adapters/primaries/rpc.validation-pipe.ts (100%) rename src/modules/{auth => authentication}/adapters/primaries/username.presenter.ts (100%) rename src/modules/{auth/adapters/secondaries/auth.messager.ts => authentication/adapters/secondaries/authentication.messager.ts} (87%) create mode 100644 src/modules/authentication/adapters/secondaries/authentication.repository.ts rename src/modules/{auth => authentication}/adapters/secondaries/logging.messager.ts (100%) rename src/modules/{auth => authentication}/adapters/secondaries/username.repository.ts (50%) rename src/modules/{auth/auth.module.ts => authentication/authentication.module.ts} (61%) rename src/modules/{auth => authentication}/commands/add-username.command.ts (100%) create mode 100644 src/modules/authentication/commands/create-authentication.command.ts create mode 100644 src/modules/authentication/commands/delete-authentication.command.ts rename src/modules/{auth => authentication}/commands/delete-username.command.ts (100%) rename src/modules/{auth => authentication}/commands/update-password.command.ts (100%) rename src/modules/{auth => authentication}/commands/update-username.command.ts (100%) rename src/modules/{auth => authentication}/domain/dtos/add-username.request.ts (100%) rename src/modules/{auth/domain/dtos/create-auth.request.ts => authentication/domain/dtos/create-authentication.request.ts} (90%) rename src/modules/{auth/domain/dtos/delete-auth.request.ts => authentication/domain/dtos/delete-authentication.request.ts} (79%) rename src/modules/{auth => authentication}/domain/dtos/delete-username.request.ts (100%) rename src/modules/{auth => authentication}/domain/dtos/type.enum.ts (100%) rename src/modules/{auth => authentication}/domain/dtos/update-password.request.ts (100%) rename src/modules/{auth => authentication}/domain/dtos/update-username.request.ts (100%) rename src/modules/{auth/domain/dtos/validate-auth.request.ts => authentication/domain/dtos/validate-authentication.request.ts} (78%) rename src/modules/{auth/domain/entities/auth.ts => authentication/domain/entities/authentication.ts} (76%) rename src/modules/{auth => authentication}/domain/entities/username.ts (100%) rename src/modules/{auth => authentication}/domain/interfaces/message-broker.ts (100%) rename src/modules/{auth => authentication}/domain/usecases/add-username.usecase.ts (100%) rename src/modules/{auth/domain/usecases/create-auth.usecase.ts => authentication/domain/usecases/create-authentication.usecase.ts} (55%) rename src/modules/{auth/domain/usecases/delete-auth.usecase.ts => authentication/domain/usecases/delete-authentication.usecase.ts} (51%) rename src/modules/{auth => authentication}/domain/usecases/delete-username.usecase.ts (100%) rename src/modules/{auth => authentication}/domain/usecases/update-password.usecase.ts (67%) rename src/modules/{auth => authentication}/domain/usecases/update-username.usecase.ts (100%) rename src/modules/{auth/domain/usecases/validate-auth.usecase.ts => authentication/domain/usecases/validate-authentication.usecase.ts} (59%) rename src/modules/{auth/mappers/auth.profile.ts => authentication/mappers/authentication.profile.ts} (53%) rename src/modules/{auth => authentication}/mappers/username.profile.ts (100%) rename src/modules/{auth/queries/validate-auth.query.ts => authentication/queries/validate-authentication.query.ts} (80%) rename src/modules/{auth/tests/integration/auth.repository.spec.ts => authentication/tests/integration/authentication.repository.spec.ts} (57%) rename src/modules/{auth => authentication}/tests/integration/username.repository.spec.ts (100%) rename src/modules/{auth => authentication}/tests/unit/add-username.usecase.spec.ts (95%) create mode 100644 src/modules/authentication/tests/unit/create-authentication.usecase.spec.ts rename src/modules/{auth/tests/unit/delete-auth.usecase.spec.ts => authentication/tests/unit/delete-authentication.usecase.spec.ts} (53%) rename src/modules/{auth => authentication}/tests/unit/delete-username.usecase.spec.ts (100%) rename src/modules/{auth => authentication}/tests/unit/update-password.usecase.spec.ts (85%) rename src/modules/{auth => authentication}/tests/unit/update-username.usecase.spec.ts (100%) create mode 100644 src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts rename src/modules/database/src/domain/{authnz-repository.ts => auth-repository.ts} (57%) diff --git a/.env.dist b/.env.dist index 058bb27..eea390e 100644 --- a/.env.dist +++ b/.env.dist @@ -10,3 +10,6 @@ RMQ_URI=amqp://v3-broker:5672 # POSTGRES POSTGRES_IMAGE=postgres:15.0 + +# OPA +OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless diff --git a/.env.test b/.env.test index 215555a..a23fa4c 100644 --- a/.env.test +++ b/.env.test @@ -10,3 +10,6 @@ RMQ_URI=amqp://v3-broker:5672 # POSTGRES POSTGRES_IMAGE=postgres:15.0 + +# OPA +OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless diff --git a/docker-compose.yml b/docker-compose.yml index 73217fe..ac43020 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,25 @@ services: aliases: - v3-auth-db-test + opa: + container_name: v3-opa + image: ${OPA_IMAGE} + ports: + - 8181:8181 + command: + - "run" + - "--server" + - "--log-format=json-pretty" + - "--set=decision_logs.console=true" + - "--set=default_decision=example/allow" + - "./policies/" + volumes: + - ./opa:/policies + networks: + v3-network: + aliases: + - v3-opa + networks: v3-network: name: v3-network diff --git a/opa/user/user1.rego b/opa/user/user1.rego new file mode 100644 index 0000000..4e41cb1 --- /dev/null +++ b/opa/user/user1.rego @@ -0,0 +1,7 @@ +package user1 + +default allow := false + +allow := true { + input.user == "jean" +} diff --git a/opa/user/user2.rego b/opa/user/user2.rego new file mode 100644 index 0000000..69fd097 --- /dev/null +++ b/opa/user/user2.rego @@ -0,0 +1,7 @@ +package user2 + +default allow := false + +allow := true { + input.user == "pierre" +} diff --git a/src/app.module.ts b/src/app.module.ts index d7b2a9e..be364d4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,13 +2,13 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { AuthModule } from './modules/auth/auth.module'; +import { AuthenticationModule } from './modules/authentication/authentication.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), AutomapperModule.forRoot({ strategyInitializer: classes() }), - AuthModule, + AuthenticationModule, ], controllers: [], providers: [], diff --git a/src/main.ts b/src/main.ts index 92a84a4..408f89b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,10 +9,10 @@ async function bootstrap() { { transport: Transport.GRPC, options: { - package: 'auth', + package: 'authentication', protoPath: join( __dirname, - 'modules/auth/adapters/primaries/auth.proto', + 'modules/auth/adapters/primaries/authentication.proto', ), url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, loader: { keepCase: true, enums: String }, diff --git a/src/modules/auth/adapters/secondaries/auth.repository.ts b/src/modules/auth/adapters/secondaries/auth.repository.ts deleted file mode 100644 index 251df9a..0000000 --- a/src/modules/auth/adapters/secondaries/auth.repository.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthNZRepository } from '../../../database/src/domain/authnz-repository'; -import { Auth } from '../../domain/entities/auth'; - -@Injectable() -export class AuthRepository extends AuthNZRepository { - protected _model = 'auth'; -} diff --git a/src/modules/auth/commands/create-auth.command.ts b/src/modules/auth/commands/create-auth.command.ts deleted file mode 100644 index 1b54ebc..0000000 --- a/src/modules/auth/commands/create-auth.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CreateAuthRequest } from '../domain/dtos/create-auth.request'; - -export class CreateAuthCommand { - readonly createAuthRequest: CreateAuthRequest; - - constructor(request: CreateAuthRequest) { - this.createAuthRequest = request; - } -} diff --git a/src/modules/auth/commands/delete-auth.command.ts b/src/modules/auth/commands/delete-auth.command.ts deleted file mode 100644 index fb3034f..0000000 --- a/src/modules/auth/commands/delete-auth.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DeleteAuthRequest } from '../domain/dtos/delete-auth.request'; - -export class DeleteAuthCommand { - readonly deleteAuthRequest: DeleteAuthRequest; - - constructor(request: DeleteAuthRequest) { - this.deleteAuthRequest = request; - } -} diff --git a/src/modules/auth/tests/unit/create-auth.usecase.spec.ts b/src/modules/auth/tests/unit/create-auth.usecase.spec.ts deleted file mode 100644 index 6d521f3..0000000 --- a/src/modules/auth/tests/unit/create-auth.usecase.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { CreateAuthCommand } from '../../commands/create-auth.command'; -import { CreateAuthRequest } from '../../domain/dtos/create-auth.request'; -import { Auth } from '../../domain/entities/auth'; -import { CreateAuthUseCase } from '../../domain/usecases/create-auth.usecase'; -import * as bcrypt from 'bcrypt'; -import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { Type } from '../../domain/dtos/type.enum'; -import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; - -const newAuthRequest: CreateAuthRequest = new CreateAuthRequest(); -newAuthRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -newAuthRequest.username = 'john.doe@email.com'; -newAuthRequest.password = 'John123'; -newAuthRequest.type = Type.EMAIL; -const newAuthCommand: CreateAuthCommand = new CreateAuthCommand(newAuthRequest); - -const mockAuthRepository = { - create: jest - .fn() - .mockImplementationOnce(() => { - return Promise.resolve({ - uuid: newAuthRequest.uuid, - password: bcrypt.hashSync(newAuthRequest.password, 10), - }); - }) - .mockImplementation(() => { - throw new Error('Already exists'); - }), -}; - -const mockUsernameRepository = { - create: jest.fn().mockResolvedValue({ - uuid: newAuthRequest.uuid, - username: newAuthRequest.username, - type: newAuthRequest.type, - }), -}; - -const mockMessager = { - publish: jest.fn().mockImplementation(), -}; - -describe('CreateAuthUseCase', () => { - let createAuthUseCase: CreateAuthUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: AuthRepository, - useValue: mockAuthRepository, - }, - { - provide: UsernameRepository, - useValue: mockUsernameRepository, - }, - { - provide: LoggingMessager, - useValue: mockMessager, - }, - CreateAuthUseCase, - ], - }).compile(); - - createAuthUseCase = module.get(CreateAuthUseCase); - }); - - it('should be defined', () => { - expect(createAuthUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should create an auth with an encrypted password', async () => { - const newAuth: Auth = await createAuthUseCase.execute(newAuthCommand); - - expect( - bcrypt.compareSync(newAuthRequest.password, newAuth.password), - ).toBeTruthy(); - }); - it('should throw an error if user already exists', async () => { - await expect( - createAuthUseCase.execute(newAuthCommand), - ).rejects.toBeInstanceOf(Error); - }); - }); -}); diff --git a/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts b/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts deleted file mode 100644 index f1bb4f6..0000000 --- a/src/modules/auth/tests/unit/validate-auth.usecase.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { Auth } from '../../domain/entities/auth'; -import * as bcrypt from 'bcrypt'; -import { ValidateAuthUseCase } from '../../domain/usecases/validate-auth.usecase'; -import { ValidateAuthQuery } from '../../queries/validate-auth.query'; -import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { Type } from '../../domain/dtos/type.enum'; -import { NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; -import { ValidateAuthRequest } from '../../domain/dtos/validate-auth.request'; - -const mockAuthRepository = { - findOne: jest - .fn() - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - password: bcrypt.hashSync('John123', 10), - })) - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - password: bcrypt.hashSync('John123', 10), - })), -}; - -const mockUsernameRepository = { - findOne: jest - .fn() - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', - type: Type.EMAIL, - })) - .mockImplementationOnce(() => { - throw new DatabaseException(); - }) - .mockImplementationOnce(() => ({ - uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', - username: 'john.doe@email.com', - type: Type.EMAIL, - })), -}; - -describe('ValidateAuthUseCase', () => { - let validateAuthUseCase: ValidateAuthUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ - { - provide: AuthRepository, - useValue: mockAuthRepository, - }, - { - provide: UsernameRepository, - useValue: mockUsernameRepository, - }, - ValidateAuthUseCase, - ], - }).compile(); - - validateAuthUseCase = module.get(ValidateAuthUseCase); - }); - - it('should be defined', () => { - expect(validateAuthUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should validate an auth and returns entity object', async () => { - const validateAuthRequest: ValidateAuthRequest = - new ValidateAuthRequest(); - validateAuthRequest.username = 'john.doe@email.com'; - validateAuthRequest.password = 'John123'; - const auth: Auth = await validateAuthUseCase.execute( - new ValidateAuthQuery( - validateAuthRequest.username, - validateAuthRequest.password, - ), - ); - - expect(auth.uuid).toBe('bb281075-1b98-4456-89d6-c643d3044a91'); - }); - - it('should not validate an auth with unknown username and returns not found exception', async () => { - await expect( - validateAuthUseCase.execute( - new ValidateAuthQuery('jane.doe@email.com', 'Jane123'), - ), - ).rejects.toBeInstanceOf(NotFoundException); - }); - - it('should not validate an auth with wrong password and returns unauthorized exception', async () => { - await expect( - validateAuthUseCase.execute( - new ValidateAuthQuery('john.doe@email.com', 'John1234'), - ), - ).rejects.toBeInstanceOf(UnauthorizedException); - }); - }); -}); diff --git a/src/modules/auth/adapters/primaries/auth-messager.controller.ts b/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts similarity index 82% rename from src/modules/auth/adapters/primaries/auth-messager.controller.ts rename to src/modules/authentication/adapters/primaries/authentication-messager.controller.ts index 7331e7b..b360deb 100644 --- a/src/modules/auth/adapters/primaries/auth-messager.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts @@ -4,11 +4,11 @@ import { CommandBus } from '@nestjs/cqrs'; import { UpdateUsernameCommand } from '../../commands/update-username.command'; import { Type } from '../../domain/dtos/type.enum'; import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request'; -import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request'; -import { DeleteAuthCommand } from '../../commands/delete-auth.command'; +import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request'; +import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; @Controller() -export class AuthMessagerController { +export class AuthenticationMessagerController { constructor(private readonly _commandBus: CommandBus) {} @RabbitSubscribe({ @@ -47,8 +47,10 @@ export class AuthMessagerController { public async userDeletedHandler(message: string) { const deletedUser = JSON.parse(message); if (!deletedUser.hasOwnProperty('uuid')) throw new Error(); - const deleteAuthRequest = new DeleteAuthRequest(); + const deleteAuthRequest = new DeleteAuthenticationRequest(); deleteAuthRequest.uuid = deletedUser.uuid; - await this._commandBus.execute(new DeleteAuthCommand(deleteAuthRequest)); + await this._commandBus.execute( + new DeleteAuthenticationCommand(deleteAuthRequest), + ); } } diff --git a/src/modules/auth/adapters/primaries/auth.controller.ts b/src/modules/authentication/adapters/primaries/authentication.controller.ts similarity index 71% rename from src/modules/auth/adapters/primaries/auth.controller.ts rename to src/modules/authentication/adapters/primaries/authentication.controller.ts index 515b62c..d78f6d0 100644 --- a/src/modules/auth/adapters/primaries/auth.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication.controller.ts @@ -5,22 +5,22 @@ import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException'; import { AddUsernameCommand } from '../../commands/add-username.command'; -import { CreateAuthCommand } from '../../commands/create-auth.command'; -import { DeleteAuthCommand } from '../../commands/delete-auth.command'; +import { CreateAuthenticationCommand } from '../../commands/create-authentication.command'; +import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; import { DeleteUsernameCommand } from '../../commands/delete-username.command'; import { UpdatePasswordCommand } from '../../commands/update-password.command'; import { UpdateUsernameCommand } from '../../commands/update-username.command'; import { AddUsernameRequest } from '../../domain/dtos/add-username.request'; -import { CreateAuthRequest } from '../../domain/dtos/create-auth.request'; -import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request'; +import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request'; +import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request'; import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request'; import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request'; import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request'; -import { ValidateAuthRequest } from '../../domain/dtos/validate-auth.request'; -import { Auth } from '../../domain/entities/auth'; +import { ValidateAuthRequest } from '../../domain/dtos/validate-authentication.request'; +import { Authentication } from '../../domain/entities/authentication'; import { Username } from '../../domain/entities/username'; -import { ValidateAuthQuery } from '../../queries/validate-auth.query'; -import { AuthPresenter } from './auth.presenter'; +import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; +import { AuthenticationPresenter } from './authentication.presenter'; import { RpcValidationPipe } from './rpc.validation-pipe'; import { UsernamePresenter } from './username.presenter'; @@ -31,7 +31,7 @@ import { UsernamePresenter } from './username.presenter'; }), ) @Controller() -export class AuthController { +export class AuthenticationController { constructor( private readonly _commandBus: CommandBus, private readonly _queryBus: QueryBus, @@ -39,12 +39,12 @@ export class AuthController { ) {} @GrpcMethod('AuthService', 'Validate') - async validate(data: ValidateAuthRequest): Promise { + async validate(data: ValidateAuthRequest): Promise { try { - const auth: Auth = await this._queryBus.execute( - new ValidateAuthQuery(data.username, data.password), + const auth: Authentication = await this._queryBus.execute( + new ValidateAuthenticationQuery(data.username, data.password), ); - return this._mapper.map(auth, Auth, AuthPresenter); + return this._mapper.map(auth, Authentication, AuthenticationPresenter); } catch (e) { throw new RpcException({ code: 7, @@ -54,12 +54,14 @@ export class AuthController { } @GrpcMethod('AuthService', 'Create') - async createUser(data: CreateAuthRequest): Promise { + async createUser( + data: CreateAuthenticationRequest, + ): Promise { try { - const auth: Auth = await this._commandBus.execute( - new CreateAuthCommand(data), + const auth: Authentication = await this._commandBus.execute( + new CreateAuthenticationCommand(data), ); - return this._mapper.map(auth, Auth, AuthPresenter); + return this._mapper.map(auth, Authentication, AuthenticationPresenter); } catch (e) { if (e instanceof DatabaseException) { if (e.message.includes('Already exists')) { @@ -127,13 +129,15 @@ export class AuthController { } @GrpcMethod('AuthService', 'UpdatePassword') - async updatePassword(data: UpdatePasswordRequest): Promise { + async updatePassword( + data: UpdatePasswordRequest, + ): Promise { try { - const auth: Auth = await this._commandBus.execute( + const auth: Authentication = await this._commandBus.execute( new UpdatePasswordCommand(data), ); - return this._mapper.map(auth, Auth, AuthPresenter); + return this._mapper.map(auth, Authentication, AuthenticationPresenter); } catch (e) { throw new RpcException({ code: 7, @@ -155,9 +159,11 @@ export class AuthController { } @GrpcMethod('AuthService', 'Delete') - async deleteAuth(data: DeleteAuthRequest) { + async deleteAuth(data: DeleteAuthenticationRequest) { try { - return await this._commandBus.execute(new DeleteAuthCommand(data)); + return await this._commandBus.execute( + new DeleteAuthenticationCommand(data), + ); } catch (e) { throw new RpcException({ code: 7, diff --git a/src/modules/auth/adapters/primaries/auth.presenter.ts b/src/modules/authentication/adapters/primaries/authentication.presenter.ts similarity index 66% rename from src/modules/auth/adapters/primaries/auth.presenter.ts rename to src/modules/authentication/adapters/primaries/authentication.presenter.ts index b780968..080c7eb 100644 --- a/src/modules/auth/adapters/primaries/auth.presenter.ts +++ b/src/modules/authentication/adapters/primaries/authentication.presenter.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; -export class AuthPresenter { +export class AuthenticationPresenter { @AutoMap() uuid: string; } diff --git a/src/modules/auth/adapters/primaries/auth.proto b/src/modules/authentication/adapters/primaries/authentication.proto similarity index 72% rename from src/modules/auth/adapters/primaries/auth.proto rename to src/modules/authentication/adapters/primaries/authentication.proto index 587dd66..42c9afc 100644 --- a/src/modules/auth/adapters/primaries/auth.proto +++ b/src/modules/authentication/adapters/primaries/authentication.proto @@ -1,10 +1,10 @@ syntax = "proto3"; -package auth; +package authentication; -service AuthService { - rpc Validate(AuthByUsernamePassword) returns (Uuid); - rpc Create(Auth) returns (Uuid); +service AuthenticationService { + rpc Validate(AuthenticationByUsernamePassword) returns (Uuid); + rpc Create(Authentication) returns (Uuid); rpc AddUsername(Username) returns (Uuid); rpc UpdatePassword(Password) returns (Uuid); rpc UpdateUsername(Username) returns (Uuid); @@ -12,7 +12,7 @@ service AuthService { rpc Delete(Uuid) returns (Empty); } -message AuthByUsernamePassword { +message AuthenticationByUsernamePassword { string username = 1; string password = 2; } @@ -22,7 +22,7 @@ enum Type { PHONE = 1; } -message Auth { +message Authentication { string uuid = 1; string username = 2; string password = 3; diff --git a/src/modules/auth/adapters/primaries/rpc.validation-pipe.ts b/src/modules/authentication/adapters/primaries/rpc.validation-pipe.ts similarity index 100% rename from src/modules/auth/adapters/primaries/rpc.validation-pipe.ts rename to src/modules/authentication/adapters/primaries/rpc.validation-pipe.ts diff --git a/src/modules/auth/adapters/primaries/username.presenter.ts b/src/modules/authentication/adapters/primaries/username.presenter.ts similarity index 100% rename from src/modules/auth/adapters/primaries/username.presenter.ts rename to src/modules/authentication/adapters/primaries/username.presenter.ts diff --git a/src/modules/auth/adapters/secondaries/auth.messager.ts b/src/modules/authentication/adapters/secondaries/authentication.messager.ts similarity index 87% rename from src/modules/auth/adapters/secondaries/auth.messager.ts rename to src/modules/authentication/adapters/secondaries/authentication.messager.ts index 7bf4e2e..7364946 100644 --- a/src/modules/auth/adapters/secondaries/auth.messager.ts +++ b/src/modules/authentication/adapters/secondaries/authentication.messager.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { IMessageBroker } from '../../domain/interfaces/message-broker'; @Injectable() -export class AuthMessager extends IMessageBroker { +export class AuthenticationMessager extends IMessageBroker { constructor(private readonly _amqpConnection: AmqpConnection) { super('auth'); } diff --git a/src/modules/authentication/adapters/secondaries/authentication.repository.ts b/src/modules/authentication/adapters/secondaries/authentication.repository.ts new file mode 100644 index 0000000..98d1e37 --- /dev/null +++ b/src/modules/authentication/adapters/secondaries/authentication.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; +import { AuthRepository } from '../../../database/src/domain/auth-repository'; +import { Authentication } from '../../domain/entities/authentication'; + +@Injectable() +export class AuthenticationRepository extends AuthRepository { + protected _model = 'auth'; +} diff --git a/src/modules/auth/adapters/secondaries/logging.messager.ts b/src/modules/authentication/adapters/secondaries/logging.messager.ts similarity index 100% rename from src/modules/auth/adapters/secondaries/logging.messager.ts rename to src/modules/authentication/adapters/secondaries/logging.messager.ts diff --git a/src/modules/auth/adapters/secondaries/username.repository.ts b/src/modules/authentication/adapters/secondaries/username.repository.ts similarity index 50% rename from src/modules/auth/adapters/secondaries/username.repository.ts rename to src/modules/authentication/adapters/secondaries/username.repository.ts index fe39acd..ec10d25 100644 --- a/src/modules/auth/adapters/secondaries/username.repository.ts +++ b/src/modules/authentication/adapters/secondaries/username.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { AuthNZRepository } from '../../../database/src/domain/authnz-repository'; +import { AuthRepository } from '../../../database/src/domain/auth-repository'; import { Username } from '../../domain/entities/username'; @Injectable() -export class UsernameRepository extends AuthNZRepository { +export class UsernameRepository extends AuthRepository { protected _model = 'username'; } diff --git a/src/modules/auth/auth.module.ts b/src/modules/authentication/authentication.module.ts similarity index 61% rename from src/modules/auth/auth.module.ts rename to src/modules/authentication/authentication.module.ts index cd9152a..2e5f3cb 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/authentication/authentication.module.ts @@ -1,20 +1,20 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { DatabaseModule } from '../database/database.module'; -import { AuthController } from './adapters/primaries/auth.controller'; -import { CreateAuthUseCase } from './domain/usecases/create-auth.usecase'; -import { ValidateAuthUseCase } from './domain/usecases/validate-auth.usecase'; -import { AuthProfile } from './mappers/auth.profile'; -import { AuthRepository } from './adapters/secondaries/auth.repository'; +import { AuthenticationController } from './adapters/primaries/authentication.controller'; +import { CreateAuthenticationUseCase } from './domain/usecases/create-authentication.usecase'; +import { ValidateAuthenticationUseCase } from './domain/usecases/validate-authentication.usecase'; +import { AuthenticationProfile } from './mappers/authentication.profile'; +import { AuthenticationRepository } from './adapters/secondaries/authentication.repository'; import { UpdateUsernameUseCase } from './domain/usecases/update-username.usecase'; import { UsernameProfile } from './mappers/username.profile'; import { AddUsernameUseCase } from './domain/usecases/add-username.usecase'; import { UpdatePasswordUseCase } from './domain/usecases/update-password.usecase'; import { DeleteUsernameUseCase } from './domain/usecases/delete-username.usecase'; -import { DeleteAuthUseCase } from './domain/usecases/delete-auth.usecase'; +import { DeleteAuthenticationUseCase } from './domain/usecases/delete-authentication.usecase'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AuthMessagerController } from './adapters/primaries/auth-messager.controller'; +import { AuthenticationMessagerController } from './adapters/primaries/authentication-messager.controller'; import { LoggingMessager } from './adapters/secondaries/logging.messager'; @Module({ @@ -41,20 +41,20 @@ import { LoggingMessager } from './adapters/secondaries/logging.messager'; inject: [ConfigService], }), ], - controllers: [AuthController, AuthMessagerController], + controllers: [AuthenticationController, AuthenticationMessagerController], providers: [ - AuthProfile, + AuthenticationProfile, UsernameProfile, - AuthRepository, + AuthenticationRepository, LoggingMessager, - ValidateAuthUseCase, - CreateAuthUseCase, + ValidateAuthenticationUseCase, + CreateAuthenticationUseCase, AddUsernameUseCase, UpdateUsernameUseCase, UpdatePasswordUseCase, DeleteUsernameUseCase, - DeleteAuthUseCase, + DeleteAuthenticationUseCase, ], exports: [], }) -export class AuthModule {} +export class AuthenticationModule {} diff --git a/src/modules/auth/commands/add-username.command.ts b/src/modules/authentication/commands/add-username.command.ts similarity index 100% rename from src/modules/auth/commands/add-username.command.ts rename to src/modules/authentication/commands/add-username.command.ts diff --git a/src/modules/authentication/commands/create-authentication.command.ts b/src/modules/authentication/commands/create-authentication.command.ts new file mode 100644 index 0000000..1d06deb --- /dev/null +++ b/src/modules/authentication/commands/create-authentication.command.ts @@ -0,0 +1,9 @@ +import { CreateAuthenticationRequest } from '../domain/dtos/create-authentication.request'; + +export class CreateAuthenticationCommand { + readonly createAuthenticationRequest: CreateAuthenticationRequest; + + constructor(request: CreateAuthenticationRequest) { + this.createAuthenticationRequest = request; + } +} diff --git a/src/modules/authentication/commands/delete-authentication.command.ts b/src/modules/authentication/commands/delete-authentication.command.ts new file mode 100644 index 0000000..bea3cd2 --- /dev/null +++ b/src/modules/authentication/commands/delete-authentication.command.ts @@ -0,0 +1,9 @@ +import { DeleteAuthenticationRequest } from '../domain/dtos/delete-authentication.request'; + +export class DeleteAuthenticationCommand { + readonly deleteAuthenticationRequest: DeleteAuthenticationRequest; + + constructor(request: DeleteAuthenticationRequest) { + this.deleteAuthenticationRequest = request; + } +} diff --git a/src/modules/auth/commands/delete-username.command.ts b/src/modules/authentication/commands/delete-username.command.ts similarity index 100% rename from src/modules/auth/commands/delete-username.command.ts rename to src/modules/authentication/commands/delete-username.command.ts diff --git a/src/modules/auth/commands/update-password.command.ts b/src/modules/authentication/commands/update-password.command.ts similarity index 100% rename from src/modules/auth/commands/update-password.command.ts rename to src/modules/authentication/commands/update-password.command.ts diff --git a/src/modules/auth/commands/update-username.command.ts b/src/modules/authentication/commands/update-username.command.ts similarity index 100% rename from src/modules/auth/commands/update-username.command.ts rename to src/modules/authentication/commands/update-username.command.ts diff --git a/src/modules/auth/domain/dtos/add-username.request.ts b/src/modules/authentication/domain/dtos/add-username.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/add-username.request.ts rename to src/modules/authentication/domain/dtos/add-username.request.ts diff --git a/src/modules/auth/domain/dtos/create-auth.request.ts b/src/modules/authentication/domain/dtos/create-authentication.request.ts similarity index 90% rename from src/modules/auth/domain/dtos/create-auth.request.ts rename to src/modules/authentication/domain/dtos/create-authentication.request.ts index 91423cb..7ca6562 100644 --- a/src/modules/auth/domain/dtos/create-auth.request.ts +++ b/src/modules/authentication/domain/dtos/create-authentication.request.ts @@ -2,7 +2,7 @@ import { AutoMap } from '@automapper/classes'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { Type } from './type.enum'; -export class CreateAuthRequest { +export class CreateAuthenticationRequest { @IsString() @IsNotEmpty() @AutoMap() diff --git a/src/modules/auth/domain/dtos/delete-auth.request.ts b/src/modules/authentication/domain/dtos/delete-authentication.request.ts similarity index 79% rename from src/modules/auth/domain/dtos/delete-auth.request.ts rename to src/modules/authentication/domain/dtos/delete-authentication.request.ts index ac92806..09ecb56 100644 --- a/src/modules/auth/domain/dtos/delete-auth.request.ts +++ b/src/modules/authentication/domain/dtos/delete-authentication.request.ts @@ -1,7 +1,7 @@ import { AutoMap } from '@automapper/classes'; import { IsNotEmpty, IsString } from 'class-validator'; -export class DeleteAuthRequest { +export class DeleteAuthenticationRequest { @IsString() @IsNotEmpty() @AutoMap() diff --git a/src/modules/auth/domain/dtos/delete-username.request.ts b/src/modules/authentication/domain/dtos/delete-username.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/delete-username.request.ts rename to src/modules/authentication/domain/dtos/delete-username.request.ts diff --git a/src/modules/auth/domain/dtos/type.enum.ts b/src/modules/authentication/domain/dtos/type.enum.ts similarity index 100% rename from src/modules/auth/domain/dtos/type.enum.ts rename to src/modules/authentication/domain/dtos/type.enum.ts diff --git a/src/modules/auth/domain/dtos/update-password.request.ts b/src/modules/authentication/domain/dtos/update-password.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/update-password.request.ts rename to src/modules/authentication/domain/dtos/update-password.request.ts diff --git a/src/modules/auth/domain/dtos/update-username.request.ts b/src/modules/authentication/domain/dtos/update-username.request.ts similarity index 100% rename from src/modules/auth/domain/dtos/update-username.request.ts rename to src/modules/authentication/domain/dtos/update-username.request.ts diff --git a/src/modules/auth/domain/dtos/validate-auth.request.ts b/src/modules/authentication/domain/dtos/validate-authentication.request.ts similarity index 78% rename from src/modules/auth/domain/dtos/validate-auth.request.ts rename to src/modules/authentication/domain/dtos/validate-authentication.request.ts index 4b80587..997d67e 100644 --- a/src/modules/auth/domain/dtos/validate-auth.request.ts +++ b/src/modules/authentication/domain/dtos/validate-authentication.request.ts @@ -1,6 +1,6 @@ import { IsNotEmpty, IsString } from 'class-validator'; -export class ValidateAuthRequest { +export class ValidateAuthenticationRequest { @IsString() @IsNotEmpty() username: string; diff --git a/src/modules/auth/domain/entities/auth.ts b/src/modules/authentication/domain/entities/authentication.ts similarity index 76% rename from src/modules/auth/domain/entities/auth.ts rename to src/modules/authentication/domain/entities/authentication.ts index 4bf327d..6f00964 100644 --- a/src/modules/auth/domain/entities/auth.ts +++ b/src/modules/authentication/domain/entities/authentication.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; -export class Auth { +export class Authentication { @AutoMap() uuid: string; diff --git a/src/modules/auth/domain/entities/username.ts b/src/modules/authentication/domain/entities/username.ts similarity index 100% rename from src/modules/auth/domain/entities/username.ts rename to src/modules/authentication/domain/entities/username.ts diff --git a/src/modules/auth/domain/interfaces/message-broker.ts b/src/modules/authentication/domain/interfaces/message-broker.ts similarity index 100% rename from src/modules/auth/domain/interfaces/message-broker.ts rename to src/modules/authentication/domain/interfaces/message-broker.ts diff --git a/src/modules/auth/domain/usecases/add-username.usecase.ts b/src/modules/authentication/domain/usecases/add-username.usecase.ts similarity index 100% rename from src/modules/auth/domain/usecases/add-username.usecase.ts rename to src/modules/authentication/domain/usecases/add-username.usecase.ts diff --git a/src/modules/auth/domain/usecases/create-auth.usecase.ts b/src/modules/authentication/domain/usecases/create-authentication.usecase.ts similarity index 55% rename from src/modules/auth/domain/usecases/create-auth.usecase.ts rename to src/modules/authentication/domain/usecases/create-authentication.usecase.ts index a477ee4..7c83ff3 100644 --- a/src/modules/auth/domain/usecases/create-auth.usecase.ts +++ b/src/modules/authentication/domain/usecases/create-authentication.usecase.ts @@ -1,25 +1,25 @@ import { CommandHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { CreateAuthCommand } from '../../commands/create-auth.command'; -import { Auth } from '../entities/auth'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { CreateAuthenticationCommand } from '../../commands/create-authentication.command'; +import { Authentication } from '../entities/authentication'; import * as bcrypt from 'bcrypt'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; -@CommandHandler(CreateAuthCommand) -export class CreateAuthUseCase { +@CommandHandler(CreateAuthenticationCommand) +export class CreateAuthenticationUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _usernameRepository: UsernameRepository, private readonly _loggingMessager: LoggingMessager, ) {} - async execute(command: CreateAuthCommand): Promise { - const { uuid, password, ...username } = command.createAuthRequest; + async execute(command: CreateAuthenticationCommand): Promise { + const { uuid, password, ...username } = command.createAuthenticationRequest; const hash = await bcrypt.hash(password, 10); try { - const auth = await this._authRepository.create({ + const auth = await this._authenticationRepository.create({ uuid, password: hash, }); diff --git a/src/modules/auth/domain/usecases/delete-auth.usecase.ts b/src/modules/authentication/domain/usecases/delete-authentication.usecase.ts similarity index 51% rename from src/modules/auth/domain/usecases/delete-auth.usecase.ts rename to src/modules/authentication/domain/usecases/delete-authentication.usecase.ts index 05f4a3c..791d05e 100644 --- a/src/modules/auth/domain/usecases/delete-auth.usecase.ts +++ b/src/modules/authentication/domain/usecases/delete-authentication.usecase.ts @@ -1,23 +1,25 @@ import { CommandHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { DeleteAuthCommand } from '../../commands/delete-auth.command'; +import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; -@CommandHandler(DeleteAuthCommand) -export class DeleteAuthUseCase { +@CommandHandler(DeleteAuthenticationCommand) +export class DeleteAuthenticationUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _usernameRepository: UsernameRepository, private readonly _loggingMessager: LoggingMessager, ) {} - async execute(command: DeleteAuthCommand) { + async execute(command: DeleteAuthenticationCommand) { try { await this._usernameRepository.deleteMany({ - uuid: command.deleteAuthRequest.uuid, + uuid: command.deleteAuthenticationRequest.uuid, }); - return await this._authRepository.delete(command.deleteAuthRequest.uuid); + return await this._authenticationRepository.delete( + command.deleteAuthenticationRequest.uuid, + ); } catch (error) { this._loggingMessager.publish( 'auth.delete.crit', diff --git a/src/modules/auth/domain/usecases/delete-username.usecase.ts b/src/modules/authentication/domain/usecases/delete-username.usecase.ts similarity index 100% rename from src/modules/auth/domain/usecases/delete-username.usecase.ts rename to src/modules/authentication/domain/usecases/delete-username.usecase.ts diff --git a/src/modules/auth/domain/usecases/update-password.usecase.ts b/src/modules/authentication/domain/usecases/update-password.usecase.ts similarity index 67% rename from src/modules/auth/domain/usecases/update-password.usecase.ts rename to src/modules/authentication/domain/usecases/update-password.usecase.ts index b8bf22d..0f6f8eb 100644 --- a/src/modules/auth/domain/usecases/update-password.usecase.ts +++ b/src/modules/authentication/domain/usecases/update-password.usecase.ts @@ -1,6 +1,6 @@ import { CommandHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { Auth } from '../entities/auth'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { Authentication } from '../entities/authentication'; import * as bcrypt from 'bcrypt'; import { UpdatePasswordCommand } from '../../commands/update-password.command'; import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; @@ -8,16 +8,16 @@ import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; @CommandHandler(UpdatePasswordCommand) export class UpdatePasswordUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _loggingMessager: LoggingMessager, ) {} - async execute(command: UpdatePasswordCommand): Promise { + async execute(command: UpdatePasswordCommand): Promise { const { uuid, password } = command.updatePasswordRequest; const hash = await bcrypt.hash(password, 10); try { - return await this._authRepository.update(uuid, { + return await this._authenticationRepository.update(uuid, { password: hash, }); } catch (error) { diff --git a/src/modules/auth/domain/usecases/update-username.usecase.ts b/src/modules/authentication/domain/usecases/update-username.usecase.ts similarity index 100% rename from src/modules/auth/domain/usecases/update-username.usecase.ts rename to src/modules/authentication/domain/usecases/update-username.usecase.ts diff --git a/src/modules/auth/domain/usecases/validate-auth.usecase.ts b/src/modules/authentication/domain/usecases/validate-authentication.usecase.ts similarity index 59% rename from src/modules/auth/domain/usecases/validate-auth.usecase.ts rename to src/modules/authentication/domain/usecases/validate-authentication.usecase.ts index fd70cbe..98bb3d5 100644 --- a/src/modules/auth/domain/usecases/validate-auth.usecase.ts +++ b/src/modules/authentication/domain/usecases/validate-authentication.usecase.ts @@ -1,20 +1,22 @@ import { QueryHandler } from '@nestjs/cqrs'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { ValidateAuthQuery } from '../../queries/validate-auth.query'; -import { Auth } from '../entities/auth'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { ValidateAuthenticationQuery as ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; +import { Authentication } from '../entities/authentication'; import * as bcrypt from 'bcrypt'; import { NotFoundException, UnauthorizedException } from '@nestjs/common'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; import { Username } from '../entities/username'; -@QueryHandler(ValidateAuthQuery) -export class ValidateAuthUseCase { +@QueryHandler(ValidateAuthenticationQuery) +export class ValidateAuthenticationUseCase { constructor( - private readonly _authRepository: AuthRepository, + private readonly _authenticationRepository: AuthenticationRepository, private readonly _usernameRepository: UsernameRepository, ) {} - async execute(validate: ValidateAuthQuery): Promise { + async execute( + validate: ValidateAuthenticationQuery, + ): Promise { let username = new Username(); try { username = await this._usernameRepository.findOne({ @@ -24,7 +26,7 @@ export class ValidateAuthUseCase { throw new NotFoundException(); } try { - const auth = await this._authRepository.findOne({ + const auth = await this._authenticationRepository.findOne({ uuid: username.uuid, }); if (auth) { diff --git a/src/modules/auth/mappers/auth.profile.ts b/src/modules/authentication/mappers/authentication.profile.ts similarity index 53% rename from src/modules/auth/mappers/auth.profile.ts rename to src/modules/authentication/mappers/authentication.profile.ts index 2ceaf03..ab9156b 100644 --- a/src/modules/auth/mappers/auth.profile.ts +++ b/src/modules/authentication/mappers/authentication.profile.ts @@ -1,18 +1,18 @@ import { createMap, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { AuthPresenter } from '../adapters/primaries/auth.presenter'; -import { Auth } from '../domain/entities/auth'; +import { AuthenticationPresenter } from '../adapters/primaries/authentication.presenter'; +import { Authentication } from '../domain/entities/authentication'; @Injectable() -export class AuthProfile extends AutomapperProfile { +export class AuthenticationProfile extends AutomapperProfile { constructor(@InjectMapper() mapper: Mapper) { super(mapper); } override get profile() { return (mapper: any) => { - createMap(mapper, Auth, AuthPresenter); + createMap(mapper, Authentication, AuthenticationPresenter); }; } } diff --git a/src/modules/auth/mappers/username.profile.ts b/src/modules/authentication/mappers/username.profile.ts similarity index 100% rename from src/modules/auth/mappers/username.profile.ts rename to src/modules/authentication/mappers/username.profile.ts diff --git a/src/modules/auth/queries/validate-auth.query.ts b/src/modules/authentication/queries/validate-authentication.query.ts similarity index 80% rename from src/modules/auth/queries/validate-auth.query.ts rename to src/modules/authentication/queries/validate-authentication.query.ts index 2de952e..32bb41c 100644 --- a/src/modules/auth/queries/validate-auth.query.ts +++ b/src/modules/authentication/queries/validate-authentication.query.ts @@ -1,4 +1,4 @@ -export class ValidateAuthQuery { +export class ValidateAuthenticationQuery { readonly username: string; readonly password: string; diff --git a/src/modules/auth/tests/integration/auth.repository.spec.ts b/src/modules/authentication/tests/integration/authentication.repository.spec.ts similarity index 57% rename from src/modules/auth/tests/integration/auth.repository.spec.ts rename to src/modules/authentication/tests/integration/authentication.repository.spec.ts index 64d4f9f..3a4ded9 100644 --- a/src/modules/auth/tests/integration/auth.repository.spec.ts +++ b/src/modules/authentication/tests/integration/authentication.repository.spec.ts @@ -2,14 +2,14 @@ import { TestingModule, Test } from '@nestjs/testing'; import { DatabaseModule } from '../../../database/database.module'; import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service'; import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; import { v4 } from 'uuid'; import * as bcrypt from 'bcrypt'; -import { Auth } from '../../domain/entities/auth'; +import { Authentication } from '../../domain/entities/authentication'; -describe('AuthRepository', () => { +describe('AuthenticationRepository', () => { let prismaService: PrismaService; - let authRepository: AuthRepository; + let authenticationRepository: AuthenticationRepository; const createAuths = async (nbToCreate = 10) => { for (let i = 0; i < nbToCreate; i++) { @@ -25,11 +25,13 @@ describe('AuthRepository', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [DatabaseModule], - providers: [AuthRepository, PrismaService], + providers: [AuthenticationRepository, PrismaService], }).compile(); prismaService = module.get(PrismaService); - authRepository = module.get(AuthRepository); + authenticationRepository = module.get( + AuthenticationRepository, + ); }); afterAll(async () => { @@ -42,7 +44,7 @@ describe('AuthRepository', () => { describe('findAll', () => { it('should return an empty data array', async () => { - const res = await authRepository.findAll(); + const res = await authenticationRepository.findAll(); expect(res).toEqual({ data: [], total: 0, @@ -51,21 +53,21 @@ describe('AuthRepository', () => { it('should return a data array with 8 auths', async () => { await createAuths(8); - const auths = await authRepository.findAll(); + const auths = await authenticationRepository.findAll(); expect(auths.data.length).toBe(8); expect(auths.total).toBe(8); }); - it('should return a data array limited to 10 auths', async () => { + it('should return a data array limited to 10 authentications', async () => { await createAuths(20); - const auths = await authRepository.findAll(); + const auths = await authenticationRepository.findAll(); expect(auths.data.length).toBe(10); expect(auths.total).toBe(20); }); }); describe('findOneByUuid', () => { - it('should return an auth', async () => { + it('should return an authentication', async () => { const authToFind = await prismaService.auth.create({ data: { uuid: v4(), @@ -73,12 +75,14 @@ describe('AuthRepository', () => { }, }); - const auth = await authRepository.findOneByUuid(authToFind.uuid); + const auth = await authenticationRepository.findOneByUuid( + authToFind.uuid, + ); expect(auth.uuid).toBe(authToFind.uuid); }); it('should return null', async () => { - const auth = await authRepository.findOneByUuid( + const auth = await authenticationRepository.findOneByUuid( '544572be-11fb-4244-8235-587221fc9104', ); expect(auth).toBeNull(); @@ -86,59 +90,64 @@ describe('AuthRepository', () => { }); describe('create', () => { - it('should create an auth', async () => { + it('should create an authentication', async () => { const beforeCount = await prismaService.auth.count(); - const authToCreate: Auth = new Auth(); - authToCreate.uuid = v4(); - authToCreate.password = bcrypt.hashSync(`password`, 10); - const auth = await authRepository.create(authToCreate); + const authenticationToCreate: Authentication = new Authentication(); + authenticationToCreate.uuid = v4(); + authenticationToCreate.password = bcrypt.hashSync(`password`, 10); + const authentication = await authenticationRepository.create( + authenticationToCreate, + ); const afterCount = await prismaService.auth.count(); expect(afterCount - beforeCount).toBe(1); - expect(auth.uuid).toBeDefined(); + expect(authentication.uuid).toBeDefined(); }); }); describe('update', () => { - it('should update auth password', async () => { - const authToUpdate = await prismaService.auth.create({ + it('should update authentication password', async () => { + const authenticationToUpdate = await prismaService.auth.create({ data: { uuid: v4(), password: bcrypt.hashSync(`password`, 10), }, }); - const toUpdate: Auth = new Auth(); + const toUpdate: Authentication = new Authentication(); toUpdate.password = bcrypt.hashSync(`newPassword`, 10); - const updatedAuth = await authRepository.update( - authToUpdate.uuid, + const updatedAuthentication = await authenticationRepository.update( + authenticationToUpdate.uuid, toUpdate, ); - expect(updatedAuth.uuid).toBe(authToUpdate.uuid); + expect(updatedAuthentication.uuid).toBe(authenticationToUpdate.uuid); }); it('should throw DatabaseException', async () => { - const toUpdate: Auth = new Auth(); + const toUpdate: Authentication = new Authentication(); toUpdate.password = bcrypt.hashSync(`newPassword`, 10); await expect( - authRepository.update('544572be-11fb-4244-8235-587221fc9104', toUpdate), + authenticationRepository.update( + '544572be-11fb-4244-8235-587221fc9104', + toUpdate, + ), ).rejects.toBeInstanceOf(DatabaseException); }); }); describe('delete', () => { - it('should delete an auth', async () => { - const authToRemove = await prismaService.auth.create({ + it('should delete an authentication', async () => { + const authenticationToRemove = await prismaService.auth.create({ data: { uuid: v4(), password: bcrypt.hashSync(`password`, 10), }, }); - await authRepository.delete(authToRemove.uuid); + await authenticationRepository.delete(authenticationToRemove.uuid); const count = await prismaService.auth.count(); expect(count).toBe(0); @@ -146,7 +155,7 @@ describe('AuthRepository', () => { it('should throw DatabaseException', async () => { await expect( - authRepository.delete('544572be-11fb-4244-8235-587221fc9104'), + authenticationRepository.delete('544572be-11fb-4244-8235-587221fc9104'), ).rejects.toBeInstanceOf(DatabaseException); }); }); diff --git a/src/modules/auth/tests/integration/username.repository.spec.ts b/src/modules/authentication/tests/integration/username.repository.spec.ts similarity index 100% rename from src/modules/auth/tests/integration/username.repository.spec.ts rename to src/modules/authentication/tests/integration/username.repository.spec.ts diff --git a/src/modules/auth/tests/unit/add-username.usecase.spec.ts b/src/modules/authentication/tests/unit/add-username.usecase.spec.ts similarity index 95% rename from src/modules/auth/tests/unit/add-username.usecase.spec.ts rename to src/modules/authentication/tests/unit/add-username.usecase.spec.ts index e6a3cba..a46f35e 100644 --- a/src/modules/auth/tests/unit/add-username.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/add-username.usecase.spec.ts @@ -1,7 +1,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { AuthProfile } from '../../mappers/auth.profile'; +import { AuthenticationProfile } from '../../mappers/authentication.profile'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; import { Username } from '../../domain/entities/username'; import { Type } from '../../domain/dtos/type.enum'; @@ -50,7 +50,7 @@ describe('AddUsernameUseCase', () => { useValue: mockMessager, }, AddUsernameUseCase, - AuthProfile, + AuthenticationProfile, ], }).compile(); diff --git a/src/modules/authentication/tests/unit/create-authentication.usecase.spec.ts b/src/modules/authentication/tests/unit/create-authentication.usecase.spec.ts new file mode 100644 index 0000000..a223f69 --- /dev/null +++ b/src/modules/authentication/tests/unit/create-authentication.usecase.spec.ts @@ -0,0 +1,99 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { CreateAuthenticationCommand } from '../../commands/create-authentication.command'; +import { CreateAuthenticationRequest } from '../../domain/dtos/create-authentication.request'; +import { Authentication } from '../../domain/entities/authentication'; +import { CreateAuthenticationUseCase } from '../../domain/usecases/create-authentication.usecase'; +import * as bcrypt from 'bcrypt'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { Type } from '../../domain/dtos/type.enum'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; + +const newAuthenticationRequest: CreateAuthenticationRequest = + new CreateAuthenticationRequest(); +newAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +newAuthenticationRequest.username = 'john.doe@email.com'; +newAuthenticationRequest.password = 'John123'; +newAuthenticationRequest.type = Type.EMAIL; +const newAuthCommand: CreateAuthenticationCommand = + new CreateAuthenticationCommand(newAuthenticationRequest); + +const mockAuthenticationRepository = { + create: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve({ + uuid: newAuthenticationRequest.uuid, + password: bcrypt.hashSync(newAuthenticationRequest.password, 10), + }); + }) + .mockImplementation(() => { + throw new Error('Already exists'); + }), +}; + +const mockUsernameRepository = { + create: jest.fn().mockResolvedValue({ + uuid: newAuthenticationRequest.uuid, + username: newAuthenticationRequest.username, + type: newAuthenticationRequest.type, + }), +}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; + +describe('CreateAuthenticationUseCase', () => { + let createAuthenticationUseCase: CreateAuthenticationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, + }, + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + { + provide: LoggingMessager, + useValue: mockMessager, + }, + CreateAuthenticationUseCase, + ], + }).compile(); + + createAuthenticationUseCase = module.get( + CreateAuthenticationUseCase, + ); + }); + + it('should be defined', () => { + expect(createAuthenticationUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should create an authentication with an encrypted password', async () => { + const newAuthentication: Authentication = + await createAuthenticationUseCase.execute(newAuthCommand); + + expect( + bcrypt.compareSync( + newAuthenticationRequest.password, + newAuthentication.password, + ), + ).toBeTruthy(); + }); + it('should throw an error if user already exists', async () => { + await expect( + createAuthenticationUseCase.execute(newAuthCommand), + ).rejects.toBeInstanceOf(Error); + }); + }); +}); diff --git a/src/modules/auth/tests/unit/delete-auth.usecase.spec.ts b/src/modules/authentication/tests/unit/delete-authentication.usecase.spec.ts similarity index 53% rename from src/modules/auth/tests/unit/delete-auth.usecase.spec.ts rename to src/modules/authentication/tests/unit/delete-authentication.usecase.spec.ts index da0ce4b..dd81688 100644 --- a/src/modules/auth/tests/unit/delete-auth.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/delete-authentication.usecase.spec.ts @@ -1,13 +1,13 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; -import { DeleteAuthCommand } from '../../commands/delete-auth.command'; -import { DeleteAuthRequest } from '../../domain/dtos/delete-auth.request'; +import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; +import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentication.request'; import { Type } from '../../domain/dtos/type.enum'; -import { DeleteAuthUseCase } from '../../domain/usecases/delete-auth.usecase'; +import { DeleteAuthenticationUseCase } from '../../domain/usecases/delete-authentication.usecase'; const usernames = { data: [ @@ -25,13 +25,13 @@ const usernames = { total: 2, }; -const deleteAuthRequest: DeleteAuthRequest = new DeleteAuthRequest(); -deleteAuthRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -const deleteAuthCommand: DeleteAuthCommand = new DeleteAuthCommand( - deleteAuthRequest, -); +const deleteAuthenticationRequest: DeleteAuthenticationRequest = + new DeleteAuthenticationRequest(); +deleteAuthenticationRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +const deleteAuthenticationCommand: DeleteAuthenticationCommand = + new DeleteAuthenticationCommand(deleteAuthenticationRequest); -const mockAuthRepository = { +const mockAuthenticationRepository = { delete: jest .fn() .mockResolvedValueOnce(undefined) @@ -53,16 +53,16 @@ const mockMessager = { publish: jest.fn().mockImplementation(), }; -describe('DeleteAuthUseCase', () => { - let deleteAuthUseCase: DeleteAuthUseCase; +describe('DeleteAuthenticationUseCase', () => { + let deleteAuthenticationUseCase: DeleteAuthenticationUseCase; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], providers: [ { - provide: AuthRepository, - useValue: mockAuthRepository, + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, }, { provide: UsernameRepository, @@ -72,26 +72,30 @@ describe('DeleteAuthUseCase', () => { provide: LoggingMessager, useValue: mockMessager, }, - DeleteAuthUseCase, + DeleteAuthenticationUseCase, ], }).compile(); - deleteAuthUseCase = module.get(DeleteAuthUseCase); + deleteAuthenticationUseCase = module.get( + DeleteAuthenticationUseCase, + ); }); it('should be defined', () => { - expect(deleteAuthUseCase).toBeDefined(); + expect(deleteAuthenticationUseCase).toBeDefined(); }); describe('execute', () => { - it('should delete an auth and its usernames', async () => { - const deletedAuth = await deleteAuthUseCase.execute(deleteAuthCommand); + it('should delete an authentication and its usernames', async () => { + const deletedAuthentication = await deleteAuthenticationUseCase.execute( + deleteAuthenticationCommand, + ); - expect(deletedAuth).toBe(undefined); + expect(deletedAuthentication).toBe(undefined); }); - it('should throw an error if auth does not exist', async () => { + it('should throw an error if authentication does not exist', async () => { await expect( - deleteAuthUseCase.execute(deleteAuthCommand), + deleteAuthenticationUseCase.execute(deleteAuthenticationCommand), ).rejects.toBeInstanceOf(Error); }); }); diff --git a/src/modules/auth/tests/unit/delete-username.usecase.spec.ts b/src/modules/authentication/tests/unit/delete-username.usecase.spec.ts similarity index 100% rename from src/modules/auth/tests/unit/delete-username.usecase.spec.ts rename to src/modules/authentication/tests/unit/delete-username.usecase.spec.ts diff --git a/src/modules/auth/tests/unit/update-password.usecase.spec.ts b/src/modules/authentication/tests/unit/update-password.usecase.spec.ts similarity index 85% rename from src/modules/auth/tests/unit/update-password.usecase.spec.ts rename to src/modules/authentication/tests/unit/update-password.usecase.spec.ts index a7f4990..38ed6ac 100644 --- a/src/modules/auth/tests/unit/update-password.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/update-password.usecase.spec.ts @@ -1,8 +1,8 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; -import { AuthRepository } from '../../adapters/secondaries/auth.repository'; -import { Auth } from '../../domain/entities/auth'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { Authentication } from '../../domain/entities/authentication'; import * as bcrypt from 'bcrypt'; import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request'; import { UpdatePasswordCommand } from '../../commands/update-password.command'; @@ -18,7 +18,7 @@ const updatePasswordCommand: UpdatePasswordCommand = new UpdatePasswordCommand( updatePasswordRequest, ); -const mockAuthRepository = { +const mockAuthenticationRepository = { update: jest .fn() .mockResolvedValueOnce({ @@ -42,8 +42,8 @@ describe('UpdatePasswordUseCase', () => { imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], providers: [ { - provide: AuthRepository, - useValue: mockAuthRepository, + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, }, { provide: LoggingMessager, @@ -64,7 +64,7 @@ describe('UpdatePasswordUseCase', () => { describe('execute', () => { it('should update an auth with an new encrypted password', async () => { - const newAuth: Auth = await updatePasswordUseCase.execute( + const newAuth: Authentication = await updatePasswordUseCase.execute( updatePasswordCommand, ); diff --git a/src/modules/auth/tests/unit/update-username.usecase.spec.ts b/src/modules/authentication/tests/unit/update-username.usecase.spec.ts similarity index 100% rename from src/modules/auth/tests/unit/update-username.usecase.spec.ts rename to src/modules/authentication/tests/unit/update-username.usecase.spec.ts diff --git a/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts b/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts new file mode 100644 index 0000000..ebe8f92 --- /dev/null +++ b/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts @@ -0,0 +1,107 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; +import { Authentication } from '../../domain/entities/authentication'; +import * as bcrypt from 'bcrypt'; +import { ValidateAuthenticationUseCase } from '../../domain/usecases/validate-authentication.usecase'; +import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; +import { UsernameRepository } from '../../adapters/secondaries/username.repository'; +import { Type } from '../../domain/dtos/type.enum'; +import { NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; +import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request'; + +const mockAuthenticationRepository = { + findOne: jest + .fn() + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + password: bcrypt.hashSync('John123', 10), + })) + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + password: bcrypt.hashSync('John123', 10), + })), +}; + +const mockUsernameRepository = { + findOne: jest + .fn() + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + username: 'john.doe@email.com', + type: Type.EMAIL, + })) + .mockImplementationOnce(() => { + throw new DatabaseException(); + }) + .mockImplementationOnce(() => ({ + uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', + username: 'john.doe@email.com', + type: Type.EMAIL, + })), +}; + +describe('ValidateAuthenticationUseCase', () => { + let validateAuthenticationUseCase: ValidateAuthenticationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AuthenticationRepository, + useValue: mockAuthenticationRepository, + }, + { + provide: UsernameRepository, + useValue: mockUsernameRepository, + }, + ValidateAuthenticationUseCase, + ], + }).compile(); + + validateAuthenticationUseCase = module.get( + ValidateAuthenticationUseCase, + ); + }); + + it('should be defined', () => { + expect(validateAuthenticationUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should validate an authentication and returns entity object', async () => { + const validateAuthenticationRequest: ValidateAuthenticationRequest = + new ValidateAuthenticationRequest(); + validateAuthenticationRequest.username = 'john.doe@email.com'; + validateAuthenticationRequest.password = 'John123'; + const authentication: Authentication = + await validateAuthenticationUseCase.execute( + new ValidateAuthenticationQuery( + validateAuthenticationRequest.username, + validateAuthenticationRequest.password, + ), + ); + + expect(authentication.uuid).toBe('bb281075-1b98-4456-89d6-c643d3044a91'); + }); + + it('should not validate an authentication with unknown username and returns not found exception', async () => { + await expect( + validateAuthenticationUseCase.execute( + new ValidateAuthenticationQuery('jane.doe@email.com', 'Jane123'), + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('should not validate an authentication with wrong password and returns unauthorized exception', async () => { + await expect( + validateAuthenticationUseCase.execute( + new ValidateAuthenticationQuery('john.doe@email.com', 'John1234'), + ), + ).rejects.toBeInstanceOf(UnauthorizedException); + }); + }); +}); diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index ab31158..12fcc32 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; -import { AuthRepository } from '../auth/adapters/secondaries/auth.repository'; -import { UsernameRepository } from '../auth/adapters/secondaries/username.repository'; +import { AuthenticationRepository } from '../authentication/adapters/secondaries/authentication.repository'; +import { UsernameRepository } from '../authentication/adapters/secondaries/username.repository'; import { PrismaService } from './src/adapters/secondaries/prisma-service'; @Module({ - providers: [PrismaService, AuthRepository, UsernameRepository], - exports: [PrismaService, AuthRepository, UsernameRepository], + providers: [PrismaService, AuthenticationRepository, UsernameRepository], + exports: [PrismaService, AuthenticationRepository, UsernameRepository], }) export class DatabaseModule {} diff --git a/src/modules/database/src/domain/authnz-repository.ts b/src/modules/database/src/domain/auth-repository.ts similarity index 57% rename from src/modules/database/src/domain/authnz-repository.ts rename to src/modules/database/src/domain/auth-repository.ts index fbb71f7..e20e282 100644 --- a/src/modules/database/src/domain/authnz-repository.ts +++ b/src/modules/database/src/domain/auth-repository.ts @@ -1,3 +1,3 @@ import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract'; -export class AuthNZRepository extends PrismaRepository {} +export class AuthRepository extends PrismaRepository {} From d5be05585d99ed81a1f12ccbc96a985edde9f663 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 16:21:24 +0100 Subject: [PATCH 02/13] refactor --- opa/user/user1.rego | 2 +- opa/user/user2.rego | 2 +- .../authentication-messager.controller.ts | 10 ++++----- .../primaries/authentication.controller.ts | 22 ++++++++++--------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/opa/user/user1.rego b/opa/user/user1.rego index 4e41cb1..d2a0b36 100644 --- a/opa/user/user1.rego +++ b/opa/user/user1.rego @@ -1,4 +1,4 @@ -package user1 +package user.me default allow := false diff --git a/opa/user/user2.rego b/opa/user/user2.rego index 69fd097..980065b 100644 --- a/opa/user/user2.rego +++ b/opa/user/user2.rego @@ -1,4 +1,4 @@ -package user2 +package user.list default allow := false diff --git a/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts b/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts index b360deb..1f64072 100644 --- a/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication-messager.controller.ts @@ -14,7 +14,7 @@ export class AuthenticationMessagerController { @RabbitSubscribe({ exchange: 'user', routingKey: 'update', - queue: 'auth-user-update', + queue: 'authentication-user-update', }) public async userUpdatedHandler(message: string) { const updatedUser = JSON.parse(message); @@ -42,15 +42,15 @@ export class AuthenticationMessagerController { @RabbitSubscribe({ exchange: 'user', routingKey: 'delete', - queue: 'auth-user-delete', + queue: 'authentication-user-delete', }) public async userDeletedHandler(message: string) { const deletedUser = JSON.parse(message); if (!deletedUser.hasOwnProperty('uuid')) throw new Error(); - const deleteAuthRequest = new DeleteAuthenticationRequest(); - deleteAuthRequest.uuid = deletedUser.uuid; + const deleteAuthenticationRequest = new DeleteAuthenticationRequest(); + deleteAuthenticationRequest.uuid = deletedUser.uuid; await this._commandBus.execute( - new DeleteAuthenticationCommand(deleteAuthRequest), + new DeleteAuthenticationCommand(deleteAuthenticationRequest), ); } } diff --git a/src/modules/authentication/adapters/primaries/authentication.controller.ts b/src/modules/authentication/adapters/primaries/authentication.controller.ts index d78f6d0..437382a 100644 --- a/src/modules/authentication/adapters/primaries/authentication.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication.controller.ts @@ -16,7 +16,7 @@ import { DeleteAuthenticationRequest } from '../../domain/dtos/delete-authentica import { DeleteUsernameRequest } from '../../domain/dtos/delete-username.request'; import { UpdatePasswordRequest } from '../../domain/dtos/update-password.request'; import { UpdateUsernameRequest } from '../../domain/dtos/update-username.request'; -import { ValidateAuthRequest } from '../../domain/dtos/validate-authentication.request'; +import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request'; import { Authentication } from '../../domain/entities/authentication'; import { Username } from '../../domain/entities/username'; import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; @@ -38,8 +38,10 @@ export class AuthenticationController { @InjectMapper() private readonly _mapper: Mapper, ) {} - @GrpcMethod('AuthService', 'Validate') - async validate(data: ValidateAuthRequest): Promise { + @GrpcMethod('AuthenticationService', 'Validate') + async validate( + data: ValidateAuthenticationRequest, + ): Promise { try { const auth: Authentication = await this._queryBus.execute( new ValidateAuthenticationQuery(data.username, data.password), @@ -53,7 +55,7 @@ export class AuthenticationController { } } - @GrpcMethod('AuthService', 'Create') + @GrpcMethod('AuthenticationService', 'Create') async createUser( data: CreateAuthenticationRequest, ): Promise { @@ -78,7 +80,7 @@ export class AuthenticationController { } } - @GrpcMethod('AuthService', 'AddUsername') + @GrpcMethod('AuthenticationService', 'AddUsername') async addUsername(data: AddUsernameRequest): Promise { try { const username: Username = await this._commandBus.execute( @@ -102,7 +104,7 @@ export class AuthenticationController { } } - @GrpcMethod('AuthService', 'UpdateUsername') + @GrpcMethod('AuthenticationService', 'UpdateUsername') async updateUsername( data: UpdateUsernameRequest, ): Promise { @@ -128,7 +130,7 @@ export class AuthenticationController { } } - @GrpcMethod('AuthService', 'UpdatePassword') + @GrpcMethod('AuthenticationService', 'UpdatePassword') async updatePassword( data: UpdatePasswordRequest, ): Promise { @@ -146,7 +148,7 @@ export class AuthenticationController { } } - @GrpcMethod('AuthService', 'DeleteUsername') + @GrpcMethod('AuthenticationService', 'DeleteUsername') async deleteUsername(data: DeleteUsernameRequest) { try { return await this._commandBus.execute(new DeleteUsernameCommand(data)); @@ -158,8 +160,8 @@ export class AuthenticationController { } } - @GrpcMethod('AuthService', 'Delete') - async deleteAuth(data: DeleteAuthenticationRequest) { + @GrpcMethod('AuthenticationService', 'Delete') + async deleteAuthentication(data: DeleteAuthenticationRequest) { try { return await this._commandBus.execute( new DeleteAuthenticationCommand(data), From c832d69c2e1989ae90e1b5ffb62f86c92e2a5e75 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 17:04:43 +0100 Subject: [PATCH 03/13] start authorization module --- Dockerfile | 1 + package-lock.json | 6 +++--- src/main.ts | 2 +- .../authorization/domain/entities/authorization.ts | 4 ++++ .../domain/usecases/validate-authorization.usecase.ts | 9 +++++++++ .../queries/validate-authorization.query.ts | 9 +++++++++ 6 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/modules/authorization/domain/entities/authorization.ts create mode 100644 src/modules/authorization/domain/usecases/validate-authorization.usecase.ts create mode 100644 src/modules/authorization/queries/validate-authorization.query.ts diff --git a/Dockerfile b/Dockerfile index 147fe0f..85927e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ COPY --chown=node:node package*.json ./ # Install app dependencies using the `npm ci` command instead of `npm install` RUN npm ci +RUN npx prisma generate # Bundle app source COPY --chown=node:node . . diff --git a/package-lock.json b/package-lock.json index d242640..0943a31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { - "name": "auth", + "name": "mobicoop-v3-auth", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "auth", + "name": "mobicoop-v3-auth", "version": "0.0.1", - "license": "UNLICENSED", + "license": "AGPL", "dependencies": { "@automapper/classes": "^8.7.7", "@automapper/core": "^8.7.7", diff --git a/src/main.ts b/src/main.ts index 408f89b..711a7a3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ async function bootstrap() { package: 'authentication', protoPath: join( __dirname, - 'modules/auth/adapters/primaries/authentication.proto', + 'modules/authentication/adapters/primaries/authentication.proto', ), url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, loader: { keepCase: true, enums: String }, diff --git a/src/modules/authorization/domain/entities/authorization.ts b/src/modules/authorization/domain/entities/authorization.ts new file mode 100644 index 0000000..bcf96ea --- /dev/null +++ b/src/modules/authorization/domain/entities/authorization.ts @@ -0,0 +1,4 @@ +export class Authorization { + uuid: string; + action: string; +} diff --git a/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts b/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts new file mode 100644 index 0000000..237f25a --- /dev/null +++ b/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts @@ -0,0 +1,9 @@ +import { QueryHandler } from '@nestjs/cqrs'; +import { ValidateAuthorizationQuery } from '../../queries/validate-authorization.query'; + +@QueryHandler(ValidateAuthorizationQuery) +export class ValidateAuthenticationUseCase { + async execute(validate: ValidateAuthorizationQuery): Promise { + return Promise.resolve(true); + } +} diff --git a/src/modules/authorization/queries/validate-authorization.query.ts b/src/modules/authorization/queries/validate-authorization.query.ts new file mode 100644 index 0000000..15c4e6d --- /dev/null +++ b/src/modules/authorization/queries/validate-authorization.query.ts @@ -0,0 +1,9 @@ +export class ValidateAuthorizationQuery { + readonly uuid: string; + readonly action: string; + + constructor(uuid: string, action: string) { + this.uuid = uuid; + this.action = action; + } +} From edcad16e3f4249fc0459e7725a5b731a03165770 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 17:07:53 +0100 Subject: [PATCH 04/13] start authorization module --- src/modules/authorization/authorization.module.ts | 9 +++++++++ .../domain/usecases/validate-authorization.usecase.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/modules/authorization/authorization.module.ts diff --git a/src/modules/authorization/authorization.module.ts b/src/modules/authorization/authorization.module.ts new file mode 100644 index 0000000..91eb2d1 --- /dev/null +++ b/src/modules/authorization/authorization.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; +import { DatabaseModule } from '../database/database.module'; + +@Module({ + imports: [DatabaseModule, CqrsModule], + exports: [], +}) +export class AuthorizationModule {} diff --git a/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts b/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts index 237f25a..9857da6 100644 --- a/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts +++ b/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts @@ -4,6 +4,6 @@ import { ValidateAuthorizationQuery } from '../../queries/validate-authorization @QueryHandler(ValidateAuthorizationQuery) export class ValidateAuthenticationUseCase { async execute(validate: ValidateAuthorizationQuery): Promise { - return Promise.resolve(true); + return Promise.resolve(validate.action == 'authorized'); } } From 1e5e0f2fbd601eae980bcd580f0ad1b4bbae76e6 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 17:15:47 +0100 Subject: [PATCH 05/13] testauthorization module --- .../validate-authorization.usecase.ts | 2 +- .../validate-authorization.usecase.spec.ts | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts diff --git a/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts b/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts index 9857da6..6551c40 100644 --- a/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts +++ b/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts @@ -2,7 +2,7 @@ import { QueryHandler } from '@nestjs/cqrs'; import { ValidateAuthorizationQuery } from '../../queries/validate-authorization.query'; @QueryHandler(ValidateAuthorizationQuery) -export class ValidateAuthenticationUseCase { +export class ValidateAuthorizationUseCase { async execute(validate: ValidateAuthorizationQuery): Promise { return Promise.resolve(validate.action == 'authorized'); } diff --git a/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts b/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts new file mode 100644 index 0000000..ad2d1c2 --- /dev/null +++ b/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts @@ -0,0 +1,23 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ValidateAuthorizationUseCase } from '../../domain/usecases/validate-authorization.usecase'; + +describe('ValidateAuthorizationUseCase', () => { + let validateAuthorizationUseCase: ValidateAuthorizationUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ValidateAuthorizationUseCase], + }).compile(); + + validateAuthorizationUseCase = module.get( + ValidateAuthorizationUseCase, + ); + }); + + it('should be defined', () => { + expect(validateAuthorizationUseCase).toBeDefined(); + }); +}); From 50742fc53a1f8b9bf6f3488ce725ce8440e4a673 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 17:25:53 +0100 Subject: [PATCH 06/13] test authorization module --- src/app.module.ts | 2 ++ src/modules/authorization/authorization.module.ts | 2 ++ .../domain/dtos/validate-authorization.request.ts | 11 +++++++++++ .../unit/validate-authorization.usecase.spec.ts | 15 +++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 src/modules/authorization/domain/dtos/validate-authorization.request.ts diff --git a/src/app.module.ts b/src/app.module.ts index be364d4..8afd292 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,12 +3,14 @@ import { AutomapperModule } from '@automapper/nestjs'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AuthenticationModule } from './modules/authentication/authentication.module'; +import { AuthorizationModule } from './modules/authorization/authorization.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), AutomapperModule.forRoot({ strategyInitializer: classes() }), AuthenticationModule, + AuthorizationModule, ], controllers: [], providers: [], diff --git a/src/modules/authorization/authorization.module.ts b/src/modules/authorization/authorization.module.ts index 91eb2d1..13dcf88 100644 --- a/src/modules/authorization/authorization.module.ts +++ b/src/modules/authorization/authorization.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; +import { ValidateAuthenticationUseCase } from '../authentication/domain/usecases/validate-authentication.usecase'; import { DatabaseModule } from '../database/database.module'; @Module({ imports: [DatabaseModule, CqrsModule], exports: [], + providers: [ValidateAuthenticationUseCase], }) export class AuthorizationModule {} diff --git a/src/modules/authorization/domain/dtos/validate-authorization.request.ts b/src/modules/authorization/domain/dtos/validate-authorization.request.ts new file mode 100644 index 0000000..8bfbf72 --- /dev/null +++ b/src/modules/authorization/domain/dtos/validate-authorization.request.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class ValidateAuthorizationRequest { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsString() + @IsNotEmpty() + action: string; +} diff --git a/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts b/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts index ad2d1c2..f68d24e 100644 --- a/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts +++ b/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts @@ -1,6 +1,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; +import { ValidateAuthorizationRequest } from '../../domain/dtos/validate-authorization.request'; import { ValidateAuthorizationUseCase } from '../../domain/usecases/validate-authorization.usecase'; describe('ValidateAuthorizationUseCase', () => { @@ -20,4 +21,18 @@ describe('ValidateAuthorizationUseCase', () => { it('should be defined', () => { expect(validateAuthorizationUseCase).toBeDefined(); }); + + describe('execute', () => { + it('should validate an authorization', async () => { + const validateAuthorizationRequest: ValidateAuthorizationRequest = + new ValidateAuthorizationRequest(); + validateAuthorizationRequest.uuid = + 'bb281075-1b98-4456-89d6-c643d3044a91'; + validateAuthorizationRequest.action = 'authorized'; + + expect( + validateAuthorizationUseCase.execute(validateAuthorizationRequest), + ).toBeTruthy(); + }); + }); }); From f67a0ef8709075238b61057a0db7b1e60b140809 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 17:33:22 +0100 Subject: [PATCH 07/13] test authorization module --- .../tests/unit/validate-authorization.usecase.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts b/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts index f68d24e..31b6173 100644 --- a/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts +++ b/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts @@ -3,6 +3,7 @@ import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidateAuthorizationRequest } from '../../domain/dtos/validate-authorization.request'; import { ValidateAuthorizationUseCase } from '../../domain/usecases/validate-authorization.usecase'; +import { ValidateAuthorizationQuery } from '../../queries/validate-authorization.query'; describe('ValidateAuthorizationUseCase', () => { let validateAuthorizationUseCase: ValidateAuthorizationUseCase; @@ -31,7 +32,12 @@ describe('ValidateAuthorizationUseCase', () => { validateAuthorizationRequest.action = 'authorized'; expect( - validateAuthorizationUseCase.execute(validateAuthorizationRequest), + validateAuthorizationUseCase.execute( + new ValidateAuthorizationQuery( + validateAuthorizationRequest.uuid, + validateAuthorizationRequest.action, + ), + ), ).toBeTruthy(); }); }); From 3d2bb613bd9673e89222a95012898bed31c12b89 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Mon, 16 Jan 2023 17:37:01 +0100 Subject: [PATCH 08/13] test authorization module --- .../tests/integration/authentication.repository.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/authentication/tests/integration/authentication.repository.spec.ts b/src/modules/authentication/tests/integration/authentication.repository.spec.ts index 3a4ded9..ffd78ce 100644 --- a/src/modules/authentication/tests/integration/authentication.repository.spec.ts +++ b/src/modules/authentication/tests/integration/authentication.repository.spec.ts @@ -11,7 +11,7 @@ describe('AuthenticationRepository', () => { let prismaService: PrismaService; let authenticationRepository: AuthenticationRepository; - const createAuths = async (nbToCreate = 10) => { + const createAuthentications = async (nbToCreate = 10) => { for (let i = 0; i < nbToCreate; i++) { await prismaService.auth.create({ data: { @@ -52,14 +52,14 @@ describe('AuthenticationRepository', () => { }); it('should return a data array with 8 auths', async () => { - await createAuths(8); + await createAuthentications(8); const auths = await authenticationRepository.findAll(); expect(auths.data.length).toBe(8); expect(auths.total).toBe(8); }); it('should return a data array limited to 10 authentications', async () => { - await createAuths(20); + await createAuthentications(20); const auths = await authenticationRepository.findAll(); expect(auths.data.length).toBe(10); expect(auths.total).toBe(20); From 972d43ac30cc1e099bc483f2fdd8acb404703571 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Tue, 17 Jan 2023 16:39:24 +0100 Subject: [PATCH 09/13] plug opa in auth --- .env.dist | 1 + .env.test | 1 + docker-compose.yml | 1 - opa/user/user1.rego | 4 +- opa/user/user2.rego | 2 +- package-lock.json | 113 ++++++++++++++++-- package.json | 8 +- src/main.ts | 16 ++- .../primaries/authorization.controller.ts | 28 +++++ .../adapters/primaries/authorization.proto | 35 ++++++ .../adapters/secondaries/decision-result.ts | 3 + .../adapters/secondaries/decision.ts | 6 + .../secondaries/opa.decision-maker.ts | 38 ++++++ .../authorization/authorization.module.ts | 10 +- .../authorization/domain/dtos/action.enum.ts | 7 ++ .../domain/dtos/decision.request.ts | 20 ++++ .../authorization/domain/dtos/domain.enum.ts | 3 + .../dtos/validate-authorization.request.ts | 11 -- .../domain/entities/authorization.ts | 4 - .../domain/interfaces/decision-maker.ts | 13 ++ .../domain/usecases/decision.usecase.ts | 17 +++ .../validate-authorization.usecase.ts | 9 -- .../authorization/queries/decision.query.ts | 21 ++++ .../queries/validate-authorization.query.ts | 9 -- .../tests/unit/decision.usecase.spec.ts | 62 ++++++++++ .../tests/unit/opa.decision-maker.spec.ts | 88 ++++++++++++++ .../validate-authorization.usecase.spec.ts | 44 ------- 27 files changed, 473 insertions(+), 101 deletions(-) create mode 100644 src/modules/authorization/adapters/primaries/authorization.controller.ts create mode 100644 src/modules/authorization/adapters/primaries/authorization.proto create mode 100644 src/modules/authorization/adapters/secondaries/decision-result.ts create mode 100644 src/modules/authorization/adapters/secondaries/decision.ts create mode 100644 src/modules/authorization/adapters/secondaries/opa.decision-maker.ts create mode 100644 src/modules/authorization/domain/dtos/action.enum.ts create mode 100644 src/modules/authorization/domain/dtos/decision.request.ts create mode 100644 src/modules/authorization/domain/dtos/domain.enum.ts delete mode 100644 src/modules/authorization/domain/dtos/validate-authorization.request.ts delete mode 100644 src/modules/authorization/domain/entities/authorization.ts create mode 100644 src/modules/authorization/domain/interfaces/decision-maker.ts create mode 100644 src/modules/authorization/domain/usecases/decision.usecase.ts delete mode 100644 src/modules/authorization/domain/usecases/validate-authorization.usecase.ts create mode 100644 src/modules/authorization/queries/decision.query.ts delete mode 100644 src/modules/authorization/queries/validate-authorization.query.ts create mode 100644 src/modules/authorization/tests/unit/decision.usecase.spec.ts create mode 100644 src/modules/authorization/tests/unit/opa.decision-maker.spec.ts delete mode 100644 src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts diff --git a/.env.dist b/.env.dist index eea390e..e55ee74 100644 --- a/.env.dist +++ b/.env.dist @@ -13,3 +13,4 @@ POSTGRES_IMAGE=postgres:15.0 # OPA OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless +OPA_URL=http://v3-opa:8181/v1/data/ diff --git a/.env.test b/.env.test index a23fa4c..00231b0 100644 --- a/.env.test +++ b/.env.test @@ -13,3 +13,4 @@ POSTGRES_IMAGE=postgres:15.0 # OPA OPA_IMAGE=openpolicyagent/opa:0.48.0-rootless +OPA_URL=http://v3-opa:8181/v1/data/ diff --git a/docker-compose.yml b/docker-compose.yml index ac43020..eafe3e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,7 +61,6 @@ services: - "--server" - "--log-format=json-pretty" - "--set=decision_logs.console=true" - - "--set=default_decision=example/allow" - "./policies/" volumes: - ./opa:/policies diff --git a/opa/user/user1.rego b/opa/user/user1.rego index d2a0b36..15fd6ea 100644 --- a/opa/user/user1.rego +++ b/opa/user/user1.rego @@ -1,7 +1,7 @@ -package user.me +package user.read default allow := false allow := true { - input.user == "jean" + input.uuid == "96d99d44-e0a6-458e-a656-de2a400d60a8" } diff --git a/opa/user/user2.rego b/opa/user/user2.rego index 980065b..f643c93 100644 --- a/opa/user/user2.rego +++ b/opa/user/user2.rego @@ -3,5 +3,5 @@ package user.list default allow := false allow := true { - input.user == "pierre" + input.uuid == "96d99d44-e0a6-458e-a656-de2a400d60a9" } diff --git a/package-lock.json b/package-lock.json index 0943a31..9130bf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@golevelup/nestjs-rabbitmq": "^3.4.0", "@grpc/grpc-js": "^1.8.0", "@grpc/proto-loader": "^0.7.4", + "@nestjs/axios": "^1.0.1", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", @@ -22,6 +23,7 @@ "@nestjs/microservices": "^9.2.1", "@nestjs/platform-express": "^9.0.0", "@prisma/client": "^4.7.1", + "axios": "^1.2.2", "bcrypt": "^5.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -1642,6 +1644,29 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@nestjs/axios": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-1.0.1.tgz", + "integrity": "sha512-TpoZM/0ZJ9xiC04qkRDFod93LCZ12TQARRU3ejDvBK2E8emvzM4HThOs5ePklVxce4Q1ZsnrIWqnImvoDmJYnQ==", + "dependencies": { + "axios": "1.2.1" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@nestjs/axios/node_modules/axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz", @@ -3173,8 +3198,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz", + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "28.1.3", @@ -3813,7 +3847,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4015,7 +4048,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4854,6 +4886,25 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "7.2.13", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz", @@ -4908,7 +4959,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7548,6 +7598,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -10487,6 +10542,26 @@ "tar": "^6.1.11" } }, + "@nestjs/axios": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-1.0.1.tgz", + "integrity": "sha512-TpoZM/0ZJ9xiC04qkRDFod93LCZ12TQARRU3ejDvBK2E8emvzM4HThOs5ePklVxce4Q1ZsnrIWqnImvoDmJYnQ==", + "requires": { + "axios": "1.2.1" + }, + "dependencies": { + "axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + } + } + }, "@nestjs/cli": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.1.5.tgz", @@ -11648,8 +11723,17 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz", + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "babel-jest": { "version": "28.1.3", @@ -12118,7 +12202,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -12278,8 +12361,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "delegates": { "version": "1.0.0", @@ -12926,6 +13008,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "fork-ts-checker-webpack-plugin": { "version": "7.2.13", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz", @@ -12962,7 +13049,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -14917,6 +15003,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index bdcf790..5c02f7d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@golevelup/nestjs-rabbitmq": "^3.4.0", "@grpc/grpc-js": "^1.8.0", "@grpc/proto-loader": "^0.7.4", + "@nestjs/axios": "^1.0.1", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", @@ -39,6 +40,7 @@ "@nestjs/microservices": "^9.2.1", "@nestjs/platform-express": "^9.0.0", "@prisma/client": "^4.7.1", + "axios": "^1.2.2", "bcrypt": "^5.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -80,7 +82,11 @@ "json", "ts" ], - "modulePathIgnorePatterns": [".controller.ts",".module.ts","main.ts"], + "modulePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + "main.ts" + ], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { diff --git a/src/main.ts b/src/main.ts index 711a7a3..88dd40c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,11 +9,17 @@ async function bootstrap() { { transport: Transport.GRPC, options: { - package: 'authentication', - protoPath: join( - __dirname, - 'modules/authentication/adapters/primaries/authentication.proto', - ), + package: ['authentication', 'authorization'], + protoPath: [ + join( + __dirname, + 'modules/authentication/adapters/primaries/authentication.proto', + ), + join( + __dirname, + 'modules/authorization/adapters/primaries/authorization.proto', + ), + ], url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, loader: { keepCase: true, enums: String }, }, diff --git a/src/modules/authorization/adapters/primaries/authorization.controller.ts b/src/modules/authorization/adapters/primaries/authorization.controller.ts new file mode 100644 index 0000000..bee6de5 --- /dev/null +++ b/src/modules/authorization/adapters/primaries/authorization.controller.ts @@ -0,0 +1,28 @@ +import { Controller } from '@nestjs/common'; +import { QueryBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { DecisionRequest } from '../../domain/dtos/decision.request'; +import { DecisionQuery } from '../../queries/decision.query'; +import { DecisionResult } from '../secondaries/decision-result'; + +@Controller() +export class AuthorizationController { + constructor(private readonly _queryBus: QueryBus) {} + + @GrpcMethod('AuthorizationService', 'Decide') + async decide(data: DecisionRequest): Promise { + try { + const decision: boolean = await this._queryBus.execute( + new DecisionQuery(data.uuid, data.domain, data.action, data.context), + ); + return { + allow: decision, + }; + } catch (e) { + throw new RpcException({ + code: 7, + message: 'Permission denied', + }); + } + } +} diff --git a/src/modules/authorization/adapters/primaries/authorization.proto b/src/modules/authorization/adapters/primaries/authorization.proto new file mode 100644 index 0000000..6e9fc61 --- /dev/null +++ b/src/modules/authorization/adapters/primaries/authorization.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package authorization; + +service AuthorizationService { + rpc Decide(AuthorizationRequest) returns (Decision); +} + +message AuthorizationRequest { + string uuid = 1; + Domain domain = 2; + Action action = 3; + repeated Item context = 4; +} + +enum Domain { + user = 0; +} + +enum Action { + create = 0; + read = 1; + update = 2; + delete = 3; + list = 4; +} + +message Item { + string name = 1; + string value = 2; +} + +message Decision { + bool allow = 1; +} diff --git a/src/modules/authorization/adapters/secondaries/decision-result.ts b/src/modules/authorization/adapters/secondaries/decision-result.ts new file mode 100644 index 0000000..547205b --- /dev/null +++ b/src/modules/authorization/adapters/secondaries/decision-result.ts @@ -0,0 +1,3 @@ +export class DecisionResult { + allow: boolean; +} diff --git a/src/modules/authorization/adapters/secondaries/decision.ts b/src/modules/authorization/adapters/secondaries/decision.ts new file mode 100644 index 0000000..ccc5e92 --- /dev/null +++ b/src/modules/authorization/adapters/secondaries/decision.ts @@ -0,0 +1,6 @@ +import { DecisionResult } from './decision-result'; + +export class Decision { + decision_id: string; + result: DecisionResult; +} diff --git a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts new file mode 100644 index 0000000..73d93c0 --- /dev/null +++ b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts @@ -0,0 +1,38 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { lastValueFrom } from 'rxjs'; +import { Action } from '../../domain/dtos/action.enum'; +import { Domain } from '../../domain/dtos/domain.enum'; +import { IMakeDecision } from '../../domain/interfaces/decision-maker'; +import { Decision } from './decision'; + +@Injectable() +export class OpaDecisionMaker extends IMakeDecision { + constructor( + private readonly configService: ConfigService, + private readonly httpService: HttpService, + ) { + super(); + } + + async decide( + uuid: string, + domain: Domain, + action: Action, + context: Array<{ name: string; value: string }>, + ): Promise { + const { data } = await lastValueFrom( + this.httpService.post( + this.configService.get('OPA_URL') + domain + '/' + action, + { + input: { + uuid, + ...context, + }, + }, + ), + ); + return data.result.allow; + } +} diff --git a/src/modules/authorization/authorization.module.ts b/src/modules/authorization/authorization.module.ts index 13dcf88..a324556 100644 --- a/src/modules/authorization/authorization.module.ts +++ b/src/modules/authorization/authorization.module.ts @@ -1,11 +1,15 @@ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; -import { ValidateAuthenticationUseCase } from '../authentication/domain/usecases/validate-authentication.usecase'; import { DatabaseModule } from '../database/database.module'; +import { AuthorizationController } from './adapters/primaries/authorization.controller'; +import { OpaDecisionMaker } from './adapters/secondaries/opa.decision-maker'; +import { DecisionUseCase } from './domain/usecases/decision.usecase'; @Module({ - imports: [DatabaseModule, CqrsModule], + imports: [DatabaseModule, CqrsModule, HttpModule], exports: [], - providers: [ValidateAuthenticationUseCase], + controllers: [AuthorizationController], + providers: [OpaDecisionMaker, DecisionUseCase], }) export class AuthorizationModule {} diff --git a/src/modules/authorization/domain/dtos/action.enum.ts b/src/modules/authorization/domain/dtos/action.enum.ts new file mode 100644 index 0000000..4a3955b --- /dev/null +++ b/src/modules/authorization/domain/dtos/action.enum.ts @@ -0,0 +1,7 @@ +export enum Action { + create = 'create', + read = 'read', + update = 'update', + delete = 'delete', + list = 'list', +} diff --git a/src/modules/authorization/domain/dtos/decision.request.ts b/src/modules/authorization/domain/dtos/decision.request.ts new file mode 100644 index 0000000..16633a6 --- /dev/null +++ b/src/modules/authorization/domain/dtos/decision.request.ts @@ -0,0 +1,20 @@ +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; +import { Action } from './action.enum'; +import { Domain } from './domain.enum'; + +export class DecisionRequest { + @IsString() + @IsNotEmpty() + uuid: string; + + @IsString() + @IsNotEmpty() + domain: Domain; + + @IsString() + @IsNotEmpty() + action: Action; + + @IsArray() + context?: Array<{ name: string; value: string }>; +} diff --git a/src/modules/authorization/domain/dtos/domain.enum.ts b/src/modules/authorization/domain/dtos/domain.enum.ts new file mode 100644 index 0000000..6cb9b92 --- /dev/null +++ b/src/modules/authorization/domain/dtos/domain.enum.ts @@ -0,0 +1,3 @@ +export enum Domain { + user = 'user', +} diff --git a/src/modules/authorization/domain/dtos/validate-authorization.request.ts b/src/modules/authorization/domain/dtos/validate-authorization.request.ts deleted file mode 100644 index 8bfbf72..0000000 --- a/src/modules/authorization/domain/dtos/validate-authorization.request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class ValidateAuthorizationRequest { - @IsString() - @IsNotEmpty() - uuid: string; - - @IsString() - @IsNotEmpty() - action: string; -} diff --git a/src/modules/authorization/domain/entities/authorization.ts b/src/modules/authorization/domain/entities/authorization.ts deleted file mode 100644 index bcf96ea..0000000 --- a/src/modules/authorization/domain/entities/authorization.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class Authorization { - uuid: string; - action: string; -} diff --git a/src/modules/authorization/domain/interfaces/decision-maker.ts b/src/modules/authorization/domain/interfaces/decision-maker.ts new file mode 100644 index 0000000..9f15363 --- /dev/null +++ b/src/modules/authorization/domain/interfaces/decision-maker.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { Action } from '../dtos/action.enum'; +import { Domain } from '../dtos/domain.enum'; + +@Injectable() +export abstract class IMakeDecision { + abstract decide( + uuid: string, + domain: Domain, + action: Action, + context: Array<{ name: string; value: string }>, + ): Promise; +} diff --git a/src/modules/authorization/domain/usecases/decision.usecase.ts b/src/modules/authorization/domain/usecases/decision.usecase.ts new file mode 100644 index 0000000..8107a1d --- /dev/null +++ b/src/modules/authorization/domain/usecases/decision.usecase.ts @@ -0,0 +1,17 @@ +import { QueryHandler } from '@nestjs/cqrs'; +import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; +import { DecisionQuery } from '../../queries/decision.query'; + +@QueryHandler(DecisionQuery) +export class DecisionUseCase { + constructor(private readonly _decisionMaker: OpaDecisionMaker) {} + + async execute(decisionQuery: DecisionQuery): Promise { + return this._decisionMaker.decide( + decisionQuery.uuid, + decisionQuery.domain, + decisionQuery.action, + decisionQuery.context, + ); + } +} diff --git a/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts b/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts deleted file mode 100644 index 6551c40..0000000 --- a/src/modules/authorization/domain/usecases/validate-authorization.usecase.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { QueryHandler } from '@nestjs/cqrs'; -import { ValidateAuthorizationQuery } from '../../queries/validate-authorization.query'; - -@QueryHandler(ValidateAuthorizationQuery) -export class ValidateAuthorizationUseCase { - async execute(validate: ValidateAuthorizationQuery): Promise { - return Promise.resolve(validate.action == 'authorized'); - } -} diff --git a/src/modules/authorization/queries/decision.query.ts b/src/modules/authorization/queries/decision.query.ts new file mode 100644 index 0000000..58cc1f3 --- /dev/null +++ b/src/modules/authorization/queries/decision.query.ts @@ -0,0 +1,21 @@ +import { Action } from '../domain/dtos/action.enum'; +import { Domain } from '../domain/dtos/domain.enum'; + +export class DecisionQuery { + readonly uuid: string; + readonly domain: Domain; + readonly action: Action; + readonly context: Array<{ name: string; value: string }>; + + constructor( + uuid: string, + domain: Domain, + action: Action, + context?: Array<{ name: string; value: string }>, + ) { + this.uuid = uuid; + this.domain = domain; + this.action = action; + this.context = context; + } +} diff --git a/src/modules/authorization/queries/validate-authorization.query.ts b/src/modules/authorization/queries/validate-authorization.query.ts deleted file mode 100644 index 15c4e6d..0000000 --- a/src/modules/authorization/queries/validate-authorization.query.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class ValidateAuthorizationQuery { - readonly uuid: string; - readonly action: string; - - constructor(uuid: string, action: string) { - this.uuid = uuid; - this.action = action; - } -} diff --git a/src/modules/authorization/tests/unit/decision.usecase.spec.ts b/src/modules/authorization/tests/unit/decision.usecase.spec.ts new file mode 100644 index 0000000..3d3a022 --- /dev/null +++ b/src/modules/authorization/tests/unit/decision.usecase.spec.ts @@ -0,0 +1,62 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; +import { Action } from '../../domain/dtos/action.enum'; +import { DecisionRequest } from '../../domain/dtos/decision.request'; +import { Domain } from '../../domain/dtos/domain.enum'; +import { DecisionUseCase } from '../../domain/usecases/decision.usecase'; +import { DecisionQuery } from '../../queries/decision.query'; + +const mockOpaDecisionMaker = { + decide: jest.fn().mockResolvedValue(Promise.resolve(true)), +}; + +describe('DecisionUseCase', () => { + let decisionUseCase: DecisionUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: OpaDecisionMaker, + useValue: mockOpaDecisionMaker, + }, + DecisionUseCase, + ], + }).compile(); + + decisionUseCase = module.get(DecisionUseCase); + }); + + it('should be defined', () => { + expect(decisionUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should validate an authorization', async () => { + const decisionRequest: DecisionRequest = new DecisionRequest(); + decisionRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; + decisionRequest.domain = Domain.user; + decisionRequest.action = Action.create; + decisionRequest.context = [ + { + name: 'context1', + value: 'value1', + }, + ]; + + expect( + decisionUseCase.execute( + new DecisionQuery( + decisionRequest.uuid, + decisionRequest.domain, + decisionRequest.action, + decisionRequest.context, + ), + ), + ).toBeTruthy(); + }); + }); +}); diff --git a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts new file mode 100644 index 0000000..97ac740 --- /dev/null +++ b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts @@ -0,0 +1,88 @@ +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { of } from 'rxjs'; +import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; +import { Action } from '../../domain/dtos/action.enum'; +import { Domain } from '../../domain/dtos/domain.enum'; + +const mockHttpService = { + post: jest + .fn() + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + decision_id: '96d99d44-e0a6-458e-a656-de2a400d60a8', + result: { + allow: true, + }, + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + decision_id: '96d99d44-e0a6-458e-a656-de2a400d60a9', + result: { + allow: false, + }, + }, + }); + }), +}; + +const mockConfigService = { + get: jest.fn().mockResolvedValue({ + OPA_URL: 'http://url/', + }), +}; + +describe('OpaDecisionMaker', () => { + let opaDecisionMaker: OpaDecisionMaker; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + { + provide: HttpService, + useValue: mockHttpService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + OpaDecisionMaker, + ], + }).compile(); + + opaDecisionMaker = module.get(OpaDecisionMaker); + }); + + it('should be defined', () => { + expect(opaDecisionMaker).toBeDefined(); + }); + + describe('execute', () => { + it('should return a truthy decision', async () => { + const decision = await opaDecisionMaker.decide( + 'bb281075-1b98-4456-89d6-c643d3044a91', + Domain.user, + Action.read, + [], + ); + expect(decision).toBeTruthy(); + }); + it('should return a falsy decision', async () => { + const decision = await opaDecisionMaker.decide( + 'bb281075-1b98-4456-89d6-c643d3044a91', + Domain.user, + Action.read, + [], + ); + expect(decision).toBeFalsy(); + }); + }); +}); diff --git a/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts b/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts deleted file mode 100644 index 31b6173..0000000 --- a/src/modules/authorization/tests/unit/validate-authorization.usecase.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { classes } from '@automapper/classes'; -import { AutomapperModule } from '@automapper/nestjs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ValidateAuthorizationRequest } from '../../domain/dtos/validate-authorization.request'; -import { ValidateAuthorizationUseCase } from '../../domain/usecases/validate-authorization.usecase'; -import { ValidateAuthorizationQuery } from '../../queries/validate-authorization.query'; - -describe('ValidateAuthorizationUseCase', () => { - let validateAuthorizationUseCase: ValidateAuthorizationUseCase; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], - providers: [ValidateAuthorizationUseCase], - }).compile(); - - validateAuthorizationUseCase = module.get( - ValidateAuthorizationUseCase, - ); - }); - - it('should be defined', () => { - expect(validateAuthorizationUseCase).toBeDefined(); - }); - - describe('execute', () => { - it('should validate an authorization', async () => { - const validateAuthorizationRequest: ValidateAuthorizationRequest = - new ValidateAuthorizationRequest(); - validateAuthorizationRequest.uuid = - 'bb281075-1b98-4456-89d6-c643d3044a91'; - validateAuthorizationRequest.action = 'authorized'; - - expect( - validateAuthorizationUseCase.execute( - new ValidateAuthorizationQuery( - validateAuthorizationRequest.uuid, - validateAuthorizationRequest.action, - ), - ), - ).toBeTruthy(); - }); - }); -}); From 1d2e7da67347864254776a8145005673e5019ee1 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 18 Jan 2023 10:47:20 +0100 Subject: [PATCH 10/13] update tests --- opa/user/{user2.rego => list.rego} | 0 opa/user/{user1.rego => read.rego} | 0 .../unit/update-username.usecase.spec.ts | 77 ++++++++++--------- .../secondaries/opa.decision-maker.ts | 11 +-- .../authorization/domain/dtos/context-item.ts | 9 +++ .../domain/dtos/decision.request.ts | 3 +- .../authorization/queries/decision.query.ts | 5 +- .../tests/unit/decision.usecase.spec.ts | 9 +-- 8 files changed, 62 insertions(+), 52 deletions(-) rename opa/user/{user2.rego => list.rego} (100%) rename opa/user/{user1.rego => read.rego} (100%) create mode 100644 src/modules/authorization/domain/dtos/context-item.ts diff --git a/opa/user/user2.rego b/opa/user/list.rego similarity index 100% rename from opa/user/user2.rego rename to opa/user/list.rego diff --git a/opa/user/user1.rego b/opa/user/read.rego similarity index 100% rename from opa/user/user1.rego rename to opa/user/read.rego diff --git a/src/modules/authentication/tests/unit/update-username.usecase.spec.ts b/src/modules/authentication/tests/unit/update-username.usecase.spec.ts index 2fcc95b..084a6b6 100644 --- a/src/modules/authentication/tests/unit/update-username.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/update-username.usecase.spec.ts @@ -18,37 +18,37 @@ const existingUsername = { type: Type.EMAIL, }; +const newUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); +newUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a90'; +newUsernameRequest.username = '+33611223344'; +newUsernameRequest.type = Type.PHONE; + const updateUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); updateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; updateUsernameRequest.username = 'johnny.doe@email.com'; updateUsernameRequest.type = Type.EMAIL; -const newUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); -newUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -newUsernameRequest.username = '+33611223344'; -newUsernameRequest.type = Type.PHONE; - -const invalidUpdateUsernameRequest: UpdateUsernameRequest = - new UpdateUsernameRequest(); -invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; -invalidUpdateUsernameRequest.username = ''; -invalidUpdateUsernameRequest.type = Type.EMAIL; - const unknownUsernameRequest: UpdateUsernameRequest = new UpdateUsernameRequest(); -unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; +unknownUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a92'; unknownUsernameRequest.username = 'unknown@email.com'; unknownUsernameRequest.type = Type.EMAIL; -const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( - updateUsernameRequest, -); +const invalidUpdateUsernameRequest: UpdateUsernameRequest = + new UpdateUsernameRequest(); +invalidUpdateUsernameRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a93'; +invalidUpdateUsernameRequest.username = ''; +invalidUpdateUsernameRequest.type = Type.EMAIL; const newUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( newUsernameRequest, ); +const updateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand( + updateUsernameRequest, +); + const invalidUpdateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(invalidUpdateUsernameRequest); @@ -56,21 +56,24 @@ const unknownUpdateUsernameCommand: UpdateUsernameCommand = new UpdateUsernameCommand(unknownUsernameRequest); const mockUsernameRepository = { - findOne: jest.fn().mockResolvedValue(existingUsername), - updateWhere: jest - .fn() - .mockImplementationOnce(() => { - return Promise.resolve(updateUsernameRequest); - }) - .mockImplementationOnce(() => { + findOne: jest.fn().mockImplementation((request) => { + if (request.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') { + return Promise.resolve(null); + } + return Promise.resolve(existingUsername); + }), + updateWhere: jest.fn().mockImplementation((request) => { + if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a90') { return Promise.resolve(newUsernameRequest); - }) - .mockImplementationOnce(() => { + } + if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a91') { + return Promise.resolve(updateUsernameRequest); + } + if (request.uuid_type.uuid == 'bb281075-1b98-4456-89d6-c643d3044a92') { throw new Error('Error'); - }) - .mockImplementationOnce(() => { - return Promise.resolve(invalidUpdateUsernameRequest); - }), + } + return Promise.resolve(invalidUpdateUsernameRequest); + }), }; const mockAddUsernameCommand = { @@ -115,15 +118,6 @@ describe('UpdateUsernameUseCase', () => { }); describe('execute', () => { - it('should update a username for email type', async () => { - const updatedUsername: Username = await updateUsernameUseCase.execute( - updateUsernameCommand, - ); - - expect(updatedUsername.username).toBe(updateUsernameRequest.username); - expect(updatedUsername.type).toBe(updateUsernameRequest.type); - }); - it('should create a new username', async () => { const newUsername: Username = await updateUsernameUseCase.execute( newUsernameCommand, @@ -133,6 +127,15 @@ describe('UpdateUsernameUseCase', () => { expect(newUsername.type).toBe(newUsernameRequest.type); }); + it('should update a username for email type', async () => { + const updatedUsername: Username = await updateUsernameUseCase.execute( + updateUsernameCommand, + ); + + expect(updatedUsername.username).toBe(updateUsernameRequest.username); + expect(updatedUsername.type).toBe(updateUsernameRequest.type); + }); + it('should throw an error if username does not exist', async () => { await expect( updateUsernameUseCase.execute(unknownUpdateUsernameCommand), diff --git a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts index 73d93c0..f4c89e4 100644 --- a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts +++ b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts @@ -5,13 +5,14 @@ import { lastValueFrom } from 'rxjs'; import { Action } from '../../domain/dtos/action.enum'; import { Domain } from '../../domain/dtos/domain.enum'; import { IMakeDecision } from '../../domain/interfaces/decision-maker'; +import { ContextItem } from '../../domain/dtos/context-item'; import { Decision } from './decision'; @Injectable() export class OpaDecisionMaker extends IMakeDecision { constructor( - private readonly configService: ConfigService, - private readonly httpService: HttpService, + private readonly _configService: ConfigService, + private readonly _httpService: HttpService, ) { super(); } @@ -20,11 +21,11 @@ export class OpaDecisionMaker extends IMakeDecision { uuid: string, domain: Domain, action: Action, - context: Array<{ name: string; value: string }>, + context: Array, ): Promise { const { data } = await lastValueFrom( - this.httpService.post( - this.configService.get('OPA_URL') + domain + '/' + action, + this._httpService.post( + this._configService.get('OPA_URL') + domain + '/' + action, { input: { uuid, diff --git a/src/modules/authorization/domain/dtos/context-item.ts b/src/modules/authorization/domain/dtos/context-item.ts new file mode 100644 index 0000000..b9b95dc --- /dev/null +++ b/src/modules/authorization/domain/dtos/context-item.ts @@ -0,0 +1,9 @@ +export class ContextItem { + name: string; + value: any; + + constructor(name: string, value: any) { + this.name = name; + this.value = value; + } +} diff --git a/src/modules/authorization/domain/dtos/decision.request.ts b/src/modules/authorization/domain/dtos/decision.request.ts index 16633a6..2dfa6ea 100644 --- a/src/modules/authorization/domain/dtos/decision.request.ts +++ b/src/modules/authorization/domain/dtos/decision.request.ts @@ -1,4 +1,5 @@ import { IsArray, IsNotEmpty, IsString } from 'class-validator'; +import { ContextItem } from './context-item'; import { Action } from './action.enum'; import { Domain } from './domain.enum'; @@ -16,5 +17,5 @@ export class DecisionRequest { action: Action; @IsArray() - context?: Array<{ name: string; value: string }>; + context?: Array; } diff --git a/src/modules/authorization/queries/decision.query.ts b/src/modules/authorization/queries/decision.query.ts index 58cc1f3..7110acf 100644 --- a/src/modules/authorization/queries/decision.query.ts +++ b/src/modules/authorization/queries/decision.query.ts @@ -1,3 +1,4 @@ +import { ContextItem } from '../domain/dtos/context-item'; import { Action } from '../domain/dtos/action.enum'; import { Domain } from '../domain/dtos/domain.enum'; @@ -5,13 +6,13 @@ export class DecisionQuery { readonly uuid: string; readonly domain: Domain; readonly action: Action; - readonly context: Array<{ name: string; value: string }>; + readonly context: Array; constructor( uuid: string, domain: Domain, action: Action, - context?: Array<{ name: string; value: string }>, + context?: Array, ) { this.uuid = uuid; this.domain = domain; diff --git a/src/modules/authorization/tests/unit/decision.usecase.spec.ts b/src/modules/authorization/tests/unit/decision.usecase.spec.ts index 3d3a022..082d283 100644 --- a/src/modules/authorization/tests/unit/decision.usecase.spec.ts +++ b/src/modules/authorization/tests/unit/decision.usecase.spec.ts @@ -3,6 +3,7 @@ import { AutomapperModule } from '@automapper/nestjs'; import { Test, TestingModule } from '@nestjs/testing'; import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; import { Action } from '../../domain/dtos/action.enum'; +import { ContextItem } from '../../domain/dtos/context-item'; import { DecisionRequest } from '../../domain/dtos/decision.request'; import { Domain } from '../../domain/dtos/domain.enum'; import { DecisionUseCase } from '../../domain/usecases/decision.usecase'; @@ -40,13 +41,7 @@ describe('DecisionUseCase', () => { decisionRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91'; decisionRequest.domain = Domain.user; decisionRequest.action = Action.create; - decisionRequest.context = [ - { - name: 'context1', - value: 'value1', - }, - ]; - + decisionRequest.context = [new ContextItem('context1', 'value1')]; expect( decisionUseCase.execute( new DecisionQuery( From 7dc6e7795f52c004e8758c44e9b774c5a1844620 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 18 Jan 2023 11:30:37 +0100 Subject: [PATCH 11/13] authorization presenter --- .../primaries/authentication.controller.ts | 26 +++++++++++----- .../primaries/authorization.controller.ts | 31 ++++++++++++++----- .../primaries/authorization.presenter.ts | 6 ++++ .../secondaries/opa.decision-maker.ts | 5 +-- .../authorization/authorization.module.ts | 3 +- .../domain/entities/authorization.ts | 10 ++++++ .../domain/interfaces/decision-maker.ts | 3 +- .../domain/usecases/decision.usecase.ts | 3 +- .../mappers/authorization.profile.ts | 18 +++++++++++ .../tests/unit/decision.usecase.spec.ts | 2 ++ .../tests/unit/opa.decision-maker.spec.ts | 12 +++---- .../pipes}/rpc.validation-pipe.ts | 0 12 files changed, 93 insertions(+), 26 deletions(-) create mode 100644 src/modules/authorization/adapters/primaries/authorization.presenter.ts create mode 100644 src/modules/authorization/domain/entities/authorization.ts create mode 100644 src/modules/authorization/mappers/authorization.profile.ts rename src/{modules/authentication/adapters/primaries => utils/pipes}/rpc.validation-pipe.ts (100%) diff --git a/src/modules/authentication/adapters/primaries/authentication.controller.ts b/src/modules/authentication/adapters/primaries/authentication.controller.ts index 437382a..5fc1901 100644 --- a/src/modules/authentication/adapters/primaries/authentication.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication.controller.ts @@ -21,7 +21,7 @@ import { Authentication } from '../../domain/entities/authentication'; import { Username } from '../../domain/entities/username'; import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; import { AuthenticationPresenter } from './authentication.presenter'; -import { RpcValidationPipe } from './rpc.validation-pipe'; +import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe'; import { UsernamePresenter } from './username.presenter'; @UsePipes( @@ -43,10 +43,14 @@ export class AuthenticationController { data: ValidateAuthenticationRequest, ): Promise { try { - const auth: Authentication = await this._queryBus.execute( + const authentication: Authentication = await this._queryBus.execute( new ValidateAuthenticationQuery(data.username, data.password), ); - return this._mapper.map(auth, Authentication, AuthenticationPresenter); + return this._mapper.map( + authentication, + Authentication, + AuthenticationPresenter, + ); } catch (e) { throw new RpcException({ code: 7, @@ -60,10 +64,14 @@ export class AuthenticationController { data: CreateAuthenticationRequest, ): Promise { try { - const auth: Authentication = await this._commandBus.execute( + const authentication: Authentication = await this._commandBus.execute( new CreateAuthenticationCommand(data), ); - return this._mapper.map(auth, Authentication, AuthenticationPresenter); + return this._mapper.map( + authentication, + Authentication, + AuthenticationPresenter, + ); } catch (e) { if (e instanceof DatabaseException) { if (e.message.includes('Already exists')) { @@ -135,11 +143,15 @@ export class AuthenticationController { data: UpdatePasswordRequest, ): Promise { try { - const auth: Authentication = await this._commandBus.execute( + const authentication: Authentication = await this._commandBus.execute( new UpdatePasswordCommand(data), ); - return this._mapper.map(auth, Authentication, AuthenticationPresenter); + return this._mapper.map( + authentication, + Authentication, + AuthenticationPresenter, + ); } catch (e) { throw new RpcException({ code: 7, diff --git a/src/modules/authorization/adapters/primaries/authorization.controller.ts b/src/modules/authorization/adapters/primaries/authorization.controller.ts index bee6de5..b975371 100644 --- a/src/modules/authorization/adapters/primaries/authorization.controller.ts +++ b/src/modules/authorization/adapters/primaries/authorization.controller.ts @@ -1,23 +1,38 @@ -import { Controller } from '@nestjs/common'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, UsePipes } from '@nestjs/common'; import { QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe'; import { DecisionRequest } from '../../domain/dtos/decision.request'; +import { Authorization } from '../../domain/entities/authorization'; import { DecisionQuery } from '../../queries/decision.query'; -import { DecisionResult } from '../secondaries/decision-result'; +import { AuthorizationPresenter } from './authorization.presenter'; +@UsePipes( + new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }), +) @Controller() export class AuthorizationController { - constructor(private readonly _queryBus: QueryBus) {} + constructor( + private readonly _queryBus: QueryBus, + @InjectMapper() private readonly _mapper: Mapper, + ) {} @GrpcMethod('AuthorizationService', 'Decide') - async decide(data: DecisionRequest): Promise { + async decide(data: DecisionRequest): Promise { try { - const decision: boolean = await this._queryBus.execute( + const authorization: Authorization = await this._queryBus.execute( new DecisionQuery(data.uuid, data.domain, data.action, data.context), ); - return { - allow: decision, - }; + return this._mapper.map( + authorization, + Authorization, + AuthorizationPresenter, + ); } catch (e) { throw new RpcException({ code: 7, diff --git a/src/modules/authorization/adapters/primaries/authorization.presenter.ts b/src/modules/authorization/adapters/primaries/authorization.presenter.ts new file mode 100644 index 0000000..c6f3733 --- /dev/null +++ b/src/modules/authorization/adapters/primaries/authorization.presenter.ts @@ -0,0 +1,6 @@ +import { AutoMap } from '@automapper/classes'; + +export class AuthorizationPresenter { + @AutoMap() + allow: boolean; +} diff --git a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts index f4c89e4..fbefa35 100644 --- a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts +++ b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts @@ -7,6 +7,7 @@ import { Domain } from '../../domain/dtos/domain.enum'; import { IMakeDecision } from '../../domain/interfaces/decision-maker'; import { ContextItem } from '../../domain/dtos/context-item'; import { Decision } from './decision'; +import { Authorization } from '../../domain/entities/authorization'; @Injectable() export class OpaDecisionMaker extends IMakeDecision { @@ -22,7 +23,7 @@ export class OpaDecisionMaker extends IMakeDecision { domain: Domain, action: Action, context: Array, - ): Promise { + ): Promise { const { data } = await lastValueFrom( this._httpService.post( this._configService.get('OPA_URL') + domain + '/' + action, @@ -34,6 +35,6 @@ export class OpaDecisionMaker extends IMakeDecision { }, ), ); - return data.result.allow; + return new Authorization(data.result.allow); } } diff --git a/src/modules/authorization/authorization.module.ts b/src/modules/authorization/authorization.module.ts index a324556..c66d76d 100644 --- a/src/modules/authorization/authorization.module.ts +++ b/src/modules/authorization/authorization.module.ts @@ -5,11 +5,12 @@ import { DatabaseModule } from '../database/database.module'; import { AuthorizationController } from './adapters/primaries/authorization.controller'; import { OpaDecisionMaker } from './adapters/secondaries/opa.decision-maker'; import { DecisionUseCase } from './domain/usecases/decision.usecase'; +import { AuthorizationProfile } from './mappers/authorization.profile'; @Module({ imports: [DatabaseModule, CqrsModule, HttpModule], exports: [], controllers: [AuthorizationController], - providers: [OpaDecisionMaker, DecisionUseCase], + providers: [OpaDecisionMaker, DecisionUseCase, AuthorizationProfile], }) export class AuthorizationModule {} diff --git a/src/modules/authorization/domain/entities/authorization.ts b/src/modules/authorization/domain/entities/authorization.ts new file mode 100644 index 0000000..3869a3b --- /dev/null +++ b/src/modules/authorization/domain/entities/authorization.ts @@ -0,0 +1,10 @@ +import { AutoMap } from '@automapper/classes'; + +export class Authorization { + @AutoMap() + allow: boolean; + + constructor(allow: boolean) { + this.allow = allow; + } +} diff --git a/src/modules/authorization/domain/interfaces/decision-maker.ts b/src/modules/authorization/domain/interfaces/decision-maker.ts index 9f15363..7a6139c 100644 --- a/src/modules/authorization/domain/interfaces/decision-maker.ts +++ b/src/modules/authorization/domain/interfaces/decision-maker.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Action } from '../dtos/action.enum'; import { Domain } from '../dtos/domain.enum'; +import { Authorization } from '../entities/authorization'; @Injectable() export abstract class IMakeDecision { @@ -9,5 +10,5 @@ export abstract class IMakeDecision { domain: Domain, action: Action, context: Array<{ name: string; value: string }>, - ): Promise; + ): Promise; } diff --git a/src/modules/authorization/domain/usecases/decision.usecase.ts b/src/modules/authorization/domain/usecases/decision.usecase.ts index 8107a1d..55e5830 100644 --- a/src/modules/authorization/domain/usecases/decision.usecase.ts +++ b/src/modules/authorization/domain/usecases/decision.usecase.ts @@ -1,12 +1,13 @@ import { QueryHandler } from '@nestjs/cqrs'; import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; import { DecisionQuery } from '../../queries/decision.query'; +import { Authorization } from '../entities/authorization'; @QueryHandler(DecisionQuery) export class DecisionUseCase { constructor(private readonly _decisionMaker: OpaDecisionMaker) {} - async execute(decisionQuery: DecisionQuery): Promise { + async execute(decisionQuery: DecisionQuery): Promise { return this._decisionMaker.decide( decisionQuery.uuid, decisionQuery.domain, diff --git a/src/modules/authorization/mappers/authorization.profile.ts b/src/modules/authorization/mappers/authorization.profile.ts new file mode 100644 index 0000000..db4419d --- /dev/null +++ b/src/modules/authorization/mappers/authorization.profile.ts @@ -0,0 +1,18 @@ +import { createMap, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { AuthorizationPresenter } from '../adapters/primaries/authorization.presenter'; +import { Authorization } from '../domain/entities/authorization'; + +@Injectable() +export class AuthorizationProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper: any) => { + createMap(mapper, Authorization, AuthorizationPresenter); + }; + } +} diff --git a/src/modules/authorization/tests/unit/decision.usecase.spec.ts b/src/modules/authorization/tests/unit/decision.usecase.spec.ts index 082d283..ce6cc23 100644 --- a/src/modules/authorization/tests/unit/decision.usecase.spec.ts +++ b/src/modules/authorization/tests/unit/decision.usecase.spec.ts @@ -7,6 +7,7 @@ import { ContextItem } from '../../domain/dtos/context-item'; import { DecisionRequest } from '../../domain/dtos/decision.request'; import { Domain } from '../../domain/dtos/domain.enum'; import { DecisionUseCase } from '../../domain/usecases/decision.usecase'; +import { AuthorizationProfile } from '../../mappers/authorization.profile'; import { DecisionQuery } from '../../queries/decision.query'; const mockOpaDecisionMaker = { @@ -25,6 +26,7 @@ describe('DecisionUseCase', () => { useValue: mockOpaDecisionMaker, }, DecisionUseCase, + AuthorizationProfile, ], }).compile(); diff --git a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts index 97ac740..33901e2 100644 --- a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts +++ b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts @@ -66,23 +66,23 @@ describe('OpaDecisionMaker', () => { }); describe('execute', () => { - it('should return a truthy decision', async () => { - const decision = await opaDecisionMaker.decide( + it('should return a truthy authorization', async () => { + const authorization = await opaDecisionMaker.decide( 'bb281075-1b98-4456-89d6-c643d3044a91', Domain.user, Action.read, [], ); - expect(decision).toBeTruthy(); + expect(authorization.allow).toBeTruthy(); }); - it('should return a falsy decision', async () => { - const decision = await opaDecisionMaker.decide( + it('should return a falsy authorization', async () => { + const authorization = await opaDecisionMaker.decide( 'bb281075-1b98-4456-89d6-c643d3044a91', Domain.user, Action.read, [], ); - expect(decision).toBeFalsy(); + expect(authorization.allow).toBeFalsy(); }); }); }); diff --git a/src/modules/authentication/adapters/primaries/rpc.validation-pipe.ts b/src/utils/pipes/rpc.validation-pipe.ts similarity index 100% rename from src/modules/authentication/adapters/primaries/rpc.validation-pipe.ts rename to src/utils/pipes/rpc.validation-pipe.ts From a578417d312ca3400e4d2746434f05405c6fb152 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 18 Jan 2023 15:50:42 +0100 Subject: [PATCH 12/13] refactor context sent to opa --- opa/user/list.rego | 2 +- opa/user/read.rego | 6 +++- .../secondaries/opa.decision-maker.ts | 30 ++++++++++++------- .../tests/unit/opa.decision-maker.spec.ts | 12 ++++++++ 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/opa/user/list.rego b/opa/user/list.rego index f643c93..4d92535 100644 --- a/opa/user/list.rego +++ b/opa/user/list.rego @@ -3,5 +3,5 @@ package user.list default allow := false allow := true { - input.uuid == "96d99d44-e0a6-458e-a656-de2a400d60a9" + input.role == "admin" } diff --git a/opa/user/read.rego b/opa/user/read.rego index 15fd6ea..16132e4 100644 --- a/opa/user/read.rego +++ b/opa/user/read.rego @@ -3,5 +3,9 @@ package user.read default allow := false allow := true { - input.uuid == "96d99d44-e0a6-458e-a656-de2a400d60a8" + input.uuid == input.owner +} + +allow := true { + input.role == "admin" } diff --git a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts index fbefa35..0e9d5e8 100644 --- a/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts +++ b/src/modules/authorization/adapters/secondaries/opa.decision-maker.ts @@ -24,17 +24,25 @@ export class OpaDecisionMaker extends IMakeDecision { action: Action, context: Array, ): Promise { - const { data } = await lastValueFrom( - this._httpService.post( - this._configService.get('OPA_URL') + domain + '/' + action, - { - input: { - uuid, - ...context, - }, - }, - ), + const reducedContext = context.reduce( + (obj, item) => Object.assign(obj, { [item.name]: item.value }), + {}, ); - return new Authorization(data.result.allow); + try { + const { data } = await lastValueFrom( + this._httpService.post( + this._configService.get('OPA_URL') + domain + '/' + action, + { + input: { + uuid, + ...reducedContext, + }, + }, + ), + ); + return new Authorization(data.result.allow); + } catch (e) { + return new Authorization(false); + } } } diff --git a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts index 33901e2..52c85e2 100644 --- a/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts +++ b/src/modules/authorization/tests/unit/opa.decision-maker.spec.ts @@ -30,6 +30,9 @@ const mockHttpService = { }, }, }); + }) + .mockImplementationOnce(() => { + throw new Error(); }), }; @@ -84,5 +87,14 @@ describe('OpaDecisionMaker', () => { ); expect(authorization.allow).toBeFalsy(); }); + it('should return a falsy authorization when an error happens', async () => { + const authorization = await opaDecisionMaker.decide( + 'bb281075-1b98-4456-89d6-c643d3044a91', + Domain.user, + Action.read, + [], + ); + expect(authorization.allow).toBeFalsy(); + }); }); }); From 9e884ec20a75d8f5b59f3078026544a454047db4 Mon Sep 17 00:00:00 2001 From: Gsk54 Date: Wed, 18 Jan 2023 16:08:11 +0100 Subject: [PATCH 13/13] update readme --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d00ced..7e82dfb 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,9 @@ npm run migrate ## Usage -The app is used for authentication (aka AuthN) and authorization (aka AuthZ : _to be developped_). +The app is used for authentication (aka AuthN) and authorization (aka AuthZ). + +### AuthN AuthN consists in verifying a username / password couple. A user can have multiple usernames (representing multiple identifiers), all of them sharing the same password. In the app, all the authentication information about a user is called an _auth_. As of 2022/10/23, the possible identifiers are : @@ -122,6 +124,41 @@ For AuthN, the app exposes the following [gRPC](https://grpc.io/) services : } ``` +### AuthZ + +AuthZ consists in verifying if a given **user** has the right permission to execute a given **action** within a given **domain**. Some context-dependant information can be given as well. + +For AuthZ, the app exposes the following [gRPC](https://grpc.io/) services : + +- **Decide** : asks the authorization service if a user has the right permission + + ```json + { + "uuid": "96d99d44-e0a6-458e-a656-de2a400d60a9", + "domain": "user", + "action": "read", + "context": [ + { + "name": "owner", + "value": "96d99d44-e0a6-458e-a656-de2a400d60a8" + }, + { + "name": "role", + "value": "admin" + } + ] + } + ``` + + In return, the service gives an authorization response : + + ```json + { + "allow": true + } + ``` + + ## Messages Various RabbitMQ messages are sent for logging purpose.