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 { 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<AuthenticationPresenter> {
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<AuthenticationPresenter> {
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<AuthenticationPresenter> {
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,

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 { 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<DecisionResult> {
async decide(data: DecisionRequest): Promise<AuthorizationPresenter> {
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,

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 { 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<ContextItem>,
): Promise<boolean> {
): Promise<Authorization> {
const { data } = await lastValueFrom(
this._httpService.post<Decision>(
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 { 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 {}

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 { 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<boolean>;
): Promise<Authorization>;
}

View File

@ -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<boolean> {
async execute(decisionQuery: DecisionQuery): Promise<Authorization> {
return this._decisionMaker.decide(
decisionQuery.uuid,
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 { 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();

View File

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