authorization presenter
This commit is contained in:
		
							parent
							
								
									1d2e7da673
								
							
						
					
					
						commit
						7dc6e7795f
					
				| 
						 | 
					@ -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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					import { AutoMap } from '@automapper/classes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AuthorizationPresenter {
 | 
				
			||||||
 | 
					  @AutoMap()
 | 
				
			||||||
 | 
					  allow: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					import { AutoMap } from '@automapper/classes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Authorization {
 | 
				
			||||||
 | 
					  @AutoMap()
 | 
				
			||||||
 | 
					  allow: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(allow: boolean) {
 | 
				
			||||||
 | 
					    this.allow = allow;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue