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