authorization presenter

This commit is contained in:
Gsk54 2023-01-18 11:30:37 +01:00
parent 1d2e7da673
commit 7dc6e7795f
12 changed files with 93 additions and 26 deletions

View File

@ -21,7 +21,7 @@ import { Authentication } from '../../domain/entities/authentication';
import { Username } from '../../domain/entities/username'; import { Username } from '../../domain/entities/username';
import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query'; import { ValidateAuthenticationQuery } from '../../queries/validate-authentication.query';
import { AuthenticationPresenter } from './authentication.presenter'; import { AuthenticationPresenter } from './authentication.presenter';
import { RpcValidationPipe } from './rpc.validation-pipe'; import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
import { UsernamePresenter } from './username.presenter'; import { UsernamePresenter } from './username.presenter';
@UsePipes( @UsePipes(
@ -43,10 +43,14 @@ export class AuthenticationController {
data: ValidateAuthenticationRequest, data: ValidateAuthenticationRequest,
): Promise<AuthenticationPresenter> { ): Promise<AuthenticationPresenter> {
try { try {
const auth: Authentication = await this._queryBus.execute( const authentication: Authentication = await this._queryBus.execute(
new ValidateAuthenticationQuery(data.username, data.password), new ValidateAuthenticationQuery(data.username, data.password),
); );
return this._mapper.map(auth, Authentication, AuthenticationPresenter); return this._mapper.map(
authentication,
Authentication,
AuthenticationPresenter,
);
} catch (e) { } catch (e) {
throw new RpcException({ throw new RpcException({
code: 7, code: 7,
@ -60,10 +64,14 @@ export class AuthenticationController {
data: CreateAuthenticationRequest, data: CreateAuthenticationRequest,
): Promise<AuthenticationPresenter> { ): Promise<AuthenticationPresenter> {
try { try {
const auth: Authentication = await this._commandBus.execute( const authentication: Authentication = await this._commandBus.execute(
new CreateAuthenticationCommand(data), new CreateAuthenticationCommand(data),
); );
return this._mapper.map(auth, Authentication, AuthenticationPresenter); return this._mapper.map(
authentication,
Authentication,
AuthenticationPresenter,
);
} catch (e) { } catch (e) {
if (e instanceof DatabaseException) { if (e instanceof DatabaseException) {
if (e.message.includes('Already exists')) { if (e.message.includes('Already exists')) {
@ -135,11 +143,15 @@ export class AuthenticationController {
data: UpdatePasswordRequest, data: UpdatePasswordRequest,
): Promise<AuthenticationPresenter> { ): Promise<AuthenticationPresenter> {
try { try {
const auth: Authentication = await this._commandBus.execute( const authentication: Authentication = await this._commandBus.execute(
new UpdatePasswordCommand(data), new UpdatePasswordCommand(data),
); );
return this._mapper.map(auth, Authentication, AuthenticationPresenter); return this._mapper.map(
authentication,
Authentication,
AuthenticationPresenter,
);
} catch (e) { } catch (e) {
throw new RpcException({ throw new RpcException({
code: 7, code: 7,

View File

@ -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 { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from 'src/utils/pipes/rpc.validation-pipe';
import { DecisionRequest } from '../../domain/dtos/decision.request'; import { DecisionRequest } from '../../domain/dtos/decision.request';
import { Authorization } from '../../domain/entities/authorization';
import { DecisionQuery } from '../../queries/decision.query'; 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() @Controller()
export class AuthorizationController { export class AuthorizationController {
constructor(private readonly _queryBus: QueryBus) {} constructor(
private readonly _queryBus: QueryBus,
@InjectMapper() private readonly _mapper: Mapper,
) {}
@GrpcMethod('AuthorizationService', 'Decide') @GrpcMethod('AuthorizationService', 'Decide')
async decide(data: DecisionRequest): Promise<DecisionResult> { async decide(data: DecisionRequest): Promise<AuthorizationPresenter> {
try { 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), new DecisionQuery(data.uuid, data.domain, data.action, data.context),
); );
return { return this._mapper.map(
allow: decision, authorization,
}; Authorization,
AuthorizationPresenter,
);
} catch (e) { } catch (e) {
throw new RpcException({ throw new RpcException({
code: 7, code: 7,

View File

@ -0,0 +1,6 @@
import { AutoMap } from '@automapper/classes';
export class AuthorizationPresenter {
@AutoMap()
allow: boolean;
}

View File

@ -7,6 +7,7 @@ import { Domain } from '../../domain/dtos/domain.enum';
import { IMakeDecision } from '../../domain/interfaces/decision-maker'; import { IMakeDecision } from '../../domain/interfaces/decision-maker';
import { ContextItem } from '../../domain/dtos/context-item'; import { ContextItem } from '../../domain/dtos/context-item';
import { Decision } from './decision'; import { Decision } from './decision';
import { Authorization } from '../../domain/entities/authorization';
@Injectable() @Injectable()
export class OpaDecisionMaker extends IMakeDecision { export class OpaDecisionMaker extends IMakeDecision {
@ -22,7 +23,7 @@ export class OpaDecisionMaker extends IMakeDecision {
domain: Domain, domain: Domain,
action: Action, action: Action,
context: Array<ContextItem>, context: Array<ContextItem>,
): Promise<boolean> { ): Promise<Authorization> {
const { data } = await lastValueFrom( const { data } = await lastValueFrom(
this._httpService.post<Decision>( this._httpService.post<Decision>(
this._configService.get<string>('OPA_URL') + domain + '/' + action, this._configService.get<string>('OPA_URL') + domain + '/' + action,
@ -34,6 +35,6 @@ export class OpaDecisionMaker extends IMakeDecision {
}, },
), ),
); );
return data.result.allow; return new Authorization(data.result.allow);
} }
} }

View File

@ -5,11 +5,12 @@ import { DatabaseModule } from '../database/database.module';
import { AuthorizationController } from './adapters/primaries/authorization.controller'; import { AuthorizationController } from './adapters/primaries/authorization.controller';
import { OpaDecisionMaker } from './adapters/secondaries/opa.decision-maker'; import { OpaDecisionMaker } from './adapters/secondaries/opa.decision-maker';
import { DecisionUseCase } from './domain/usecases/decision.usecase'; import { DecisionUseCase } from './domain/usecases/decision.usecase';
import { AuthorizationProfile } from './mappers/authorization.profile';
@Module({ @Module({
imports: [DatabaseModule, CqrsModule, HttpModule], imports: [DatabaseModule, CqrsModule, HttpModule],
exports: [], exports: [],
controllers: [AuthorizationController], controllers: [AuthorizationController],
providers: [OpaDecisionMaker, DecisionUseCase], providers: [OpaDecisionMaker, DecisionUseCase, AuthorizationProfile],
}) })
export class AuthorizationModule {} export class AuthorizationModule {}

View File

@ -0,0 +1,10 @@
import { AutoMap } from '@automapper/classes';
export class Authorization {
@AutoMap()
allow: boolean;
constructor(allow: boolean) {
this.allow = allow;
}
}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Action } from '../dtos/action.enum'; import { Action } from '../dtos/action.enum';
import { Domain } from '../dtos/domain.enum'; import { Domain } from '../dtos/domain.enum';
import { Authorization } from '../entities/authorization';
@Injectable() @Injectable()
export abstract class IMakeDecision { export abstract class IMakeDecision {
@ -9,5 +10,5 @@ export abstract class IMakeDecision {
domain: Domain, domain: Domain,
action: Action, action: Action,
context: Array<{ name: string; value: string }>, context: Array<{ name: string; value: string }>,
): Promise<boolean>; ): Promise<Authorization>;
} }

View File

@ -1,12 +1,13 @@
import { QueryHandler } from '@nestjs/cqrs'; import { QueryHandler } from '@nestjs/cqrs';
import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker'; import { OpaDecisionMaker } from '../../adapters/secondaries/opa.decision-maker';
import { DecisionQuery } from '../../queries/decision.query'; import { DecisionQuery } from '../../queries/decision.query';
import { Authorization } from '../entities/authorization';
@QueryHandler(DecisionQuery) @QueryHandler(DecisionQuery)
export class DecisionUseCase { export class DecisionUseCase {
constructor(private readonly _decisionMaker: OpaDecisionMaker) {} constructor(private readonly _decisionMaker: OpaDecisionMaker) {}
async execute(decisionQuery: DecisionQuery): Promise<boolean> { async execute(decisionQuery: DecisionQuery): Promise<Authorization> {
return this._decisionMaker.decide( return this._decisionMaker.decide(
decisionQuery.uuid, decisionQuery.uuid,
decisionQuery.domain, decisionQuery.domain,

View File

@ -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);
};
}
}

View File

@ -7,6 +7,7 @@ import { ContextItem } from '../../domain/dtos/context-item';
import { DecisionRequest } from '../../domain/dtos/decision.request'; import { DecisionRequest } from '../../domain/dtos/decision.request';
import { Domain } from '../../domain/dtos/domain.enum'; import { Domain } from '../../domain/dtos/domain.enum';
import { DecisionUseCase } from '../../domain/usecases/decision.usecase'; import { DecisionUseCase } from '../../domain/usecases/decision.usecase';
import { AuthorizationProfile } from '../../mappers/authorization.profile';
import { DecisionQuery } from '../../queries/decision.query'; import { DecisionQuery } from '../../queries/decision.query';
const mockOpaDecisionMaker = { const mockOpaDecisionMaker = {
@ -25,6 +26,7 @@ describe('DecisionUseCase', () => {
useValue: mockOpaDecisionMaker, useValue: mockOpaDecisionMaker,
}, },
DecisionUseCase, DecisionUseCase,
AuthorizationProfile,
], ],
}).compile(); }).compile();

View File

@ -66,23 +66,23 @@ describe('OpaDecisionMaker', () => {
}); });
describe('execute', () => { describe('execute', () => {
it('should return a truthy decision', async () => { it('should return a truthy authorization', async () => {
const decision = await opaDecisionMaker.decide( const authorization = await opaDecisionMaker.decide(
'bb281075-1b98-4456-89d6-c643d3044a91', 'bb281075-1b98-4456-89d6-c643d3044a91',
Domain.user, Domain.user,
Action.read, Action.read,
[], [],
); );
expect(decision).toBeTruthy(); expect(authorization.allow).toBeTruthy();
}); });
it('should return a falsy decision', async () => { it('should return a falsy authorization', async () => {
const decision = await opaDecisionMaker.decide( const authorization = await opaDecisionMaker.decide(
'bb281075-1b98-4456-89d6-c643d3044a91', 'bb281075-1b98-4456-89d6-c643d3044a91',
Domain.user, Domain.user,
Action.read, Action.read,
[], [],
); );
expect(decision).toBeFalsy(); expect(authorization.allow).toBeFalsy();
}); });
}); });
}); });