mirror of
https://gitlab.com/mobicoop/v3/service/auth.git
synced 2026-01-02 20:52:41 +00:00
plug opa in auth
This commit is contained in:
16
src/main.ts
16
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 },
|
||||
},
|
||||
|
||||
@@ -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<DecisionResult> {
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class DecisionResult {
|
||||
allow: boolean;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DecisionResult } from './decision-result';
|
||||
|
||||
export class Decision {
|
||||
decision_id: string;
|
||||
result: DecisionResult;
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
const { data } = await lastValueFrom(
|
||||
this.httpService.post<Decision>(
|
||||
this.configService.get<string>('OPA_URL') + domain + '/' + action,
|
||||
{
|
||||
input: {
|
||||
uuid,
|
||||
...context,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
return data.result.allow;
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
7
src/modules/authorization/domain/dtos/action.enum.ts
Normal file
7
src/modules/authorization/domain/dtos/action.enum.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum Action {
|
||||
create = 'create',
|
||||
read = 'read',
|
||||
update = 'update',
|
||||
delete = 'delete',
|
||||
list = 'list',
|
||||
}
|
||||
20
src/modules/authorization/domain/dtos/decision.request.ts
Normal file
20
src/modules/authorization/domain/dtos/decision.request.ts
Normal file
@@ -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 }>;
|
||||
}
|
||||
3
src/modules/authorization/domain/dtos/domain.enum.ts
Normal file
3
src/modules/authorization/domain/dtos/domain.enum.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export enum Domain {
|
||||
user = 'user',
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ValidateAuthorizationRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
action: string;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export class Authorization {
|
||||
uuid: string;
|
||||
action: string;
|
||||
}
|
||||
@@ -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<boolean>;
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
return this._decisionMaker.decide(
|
||||
decisionQuery.uuid,
|
||||
decisionQuery.domain,
|
||||
decisionQuery.action,
|
||||
decisionQuery.context,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
return Promise.resolve(validate.action == 'authorized');
|
||||
}
|
||||
}
|
||||
21
src/modules/authorization/queries/decision.query.ts
Normal file
21
src/modules/authorization/queries/decision.query.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export class ValidateAuthorizationQuery {
|
||||
readonly uuid: string;
|
||||
readonly action: string;
|
||||
|
||||
constructor(uuid: string, action: string) {
|
||||
this.uuid = uuid;
|
||||
this.action = action;
|
||||
}
|
||||
}
|
||||
@@ -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>(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>(
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user