diff --git a/nest-cli.json b/nest-cli.json index 066d065..2edc9db 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -2,7 +2,12 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "assets": ["**/*.proto"], + "assets": [ + "**/*.proto" + ], + "plugins": [ + "@nestjs/swagger" + ], "watchAssets": true } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cb12ea7..cce02db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@nestjs/schedule": "^1.1.0", "@nestjs/swagger": "^5.2.1", "@victorradael/protospack": "2.5.0", + "@victorradael/protospack-v2": "../protospack", "axios": "^0.25.0", "cron-parser": "^4.4.0", "dotenv": "^14.2.0", @@ -66,6 +67,19 @@ "node": "18.3.0" } }, + "../protospack": { + "name": "@victorradael/protospack-v2", + "version": "1.1.0", + "license": "ISC", + "dependencies": { + "rxjs": "^7.5.5", + "ts-proto": "^1.112.2" + }, + "devDependencies": { + "@types/node": "^17.0.21", + "typescript": "^4.6.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -3226,6 +3240,10 @@ "rxjs": "^7.5.5" } }, + "node_modules/@victorradael/protospack-v2": { + "resolved": "../protospack", + "link": true + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -12670,6 +12688,15 @@ "rxjs": "^7.5.5" } }, + "@victorradael/protospack-v2": { + "version": "file:../protospack", + "requires": { + "@types/node": "^17.0.21", + "rxjs": "^7.5.5", + "ts-proto": "^1.112.2", + "typescript": "^4.6.2" + } + }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", diff --git a/package.json b/package.json index 295fae1..34a2ed5 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@nestjs/schedule": "^1.1.0", "@nestjs/swagger": "^5.2.1", "@victorradael/protospack": "2.5.0", + "@victorradael/protospack-v2": "../protospack", "axios": "^0.25.0", "cron-parser": "^4.4.0", "dotenv": "^14.2.0", diff --git a/src/app.module.ts b/src/app.module.ts index 361f1be..f511377 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,7 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { ClientsModule } from '@nestjs/microservices'; import { ConfigModule } from '@nestjs/config'; -import { LoggerMiddleware } from './middlewares/authentication'; - import { InputsController } from './modules/inputs/inputs.controller'; import { TransformationsController } from './modules/transformations/transformations.controller'; import { OutputsController } from './modules/outputs/outputs.controllers'; @@ -15,18 +13,18 @@ import { InputsService } from './modules/inputs/inputs.service'; import { TransformationsService } from './modules/transformations/transformations.service'; import { OutputsService } from './modules/outputs/outputs.service'; import { PipelinesService } from './modules/pipelines/pipelines.service'; -// import { AuthService } from './modules/auth/auth.service'; import { HealthService } from './modules/health/health.service'; +import { AuthClientService } from './clients/auth/client.service'; import { InputsClientService } from './clients/inputs/client.service'; import { TransformationsClientService } from './clients/transformations/client.service'; import { OutputsClientService } from './clients/outputs/client.service'; import { PipelinesClientService } from './clients/pipelines/client.service'; -import { AuthClientService } from './clients/auth/client.service'; +import { PermissionsClientService } from './clients/permissions/client.service'; import { OutputsClientConfiguration } from './clients/outputs/client.config'; import { TransformationsClientConfiguration } from './clients/transformations/client.config'; -import { AuthClient } from './clients/auth/client.config'; +import { DucClient } from './clients/duc/client.config'; import { InputsClientConfiguration } from './clients/inputs/client.config'; import { PipelinesClientConfiguration } from './clients/pipelines/client.config'; import { CatalogController } from './modules/catalog/catalog.controller'; @@ -34,8 +32,10 @@ import { CatalogService } from './modules/catalog/catalog.service'; import { HubspotStrategy } from './modules/oauth/passport-strategies/hubspot'; import { OauthController } from './modules/oauth/oauth.controller'; import { getOauthSecrets } from './utils/OauthSecrets'; +import { AuthenticationGuard } from './authentication/authentication.guard'; +import { APP_GUARD } from '@nestjs/core'; -const authClient = new AuthClient(); +const ducClient = new DucClient(); const inputClient = new InputsClientConfiguration(); const outputClient = new OutputsClientConfiguration(); const pipelineClient = new PipelinesClientConfiguration(); @@ -58,15 +58,19 @@ const transformationClient = new TransformationsClientConfiguration(); TransformationsService, OutputsService, PipelinesService, - // AuthService, HealthService, InputsClientService, TransformationsClientService, OutputsClientService, PipelinesClientService, AuthClientService, + PermissionsClientService, CatalogService, HubspotStrategy, + { + provide: APP_GUARD, + useClass: AuthenticationGuard, + }, ], imports: [ ConfigModule.forRoot({ @@ -78,7 +82,6 @@ const transformationClient = new TransformationsClientConfiguration(); name: 'INPUTS_PACKAGE', ...inputClient.config(), }, - { name: 'TRANSFORMATIONS_PACKAGE', ...transformationClient.config(), @@ -88,8 +91,8 @@ const transformationClient = new TransformationsClientConfiguration(); ...outputClient.config(), }, { - name: 'AUTH_PACKAGE', - ...authClient.config(), + name: 'DUC_PACKAGE', + ...ducClient.config(), }, { name: 'PIPELINES_PACKAGE', @@ -98,16 +101,4 @@ const transformationClient = new TransformationsClientConfiguration(); ]), ], }) -export class AppModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer - .apply(LoggerMiddleware) - .forRoutes( - InputsController, - TransformationsController, - OutputsController, - PipelinesController, - CatalogController, - ); - } -} +export class AppModule {} diff --git a/src/authentication/authentication.decorator.ts b/src/authentication/authentication.decorator.ts new file mode 100644 index 0000000..e840f54 --- /dev/null +++ b/src/authentication/authentication.decorator.ts @@ -0,0 +1,28 @@ +import { SetMetadata, applyDecorators } from '@nestjs/common'; +import { Request } from 'express'; +import { Permission } from './permissions.enum'; + +export const PERMISSIONS_KEY = '__PERMISSIONS__'; +export const MUST_BE_AUTHENTICATED_KEY = '__MUST_BE_AUTHENTICATED__'; +export const CUSTOM_AUTHENTICATION_FUNCTION_KEY = + '__CUSTOM_AUTHENTICATION_FUNCTION__'; + +export type AuthenticationFunction = (req: Request, user: any) => boolean; + +export function RequirePermissions(...permissions: Permission[]) { + return applyDecorators( + SetMetadata(PERMISSIONS_KEY, permissions), + SetMetadata(MUST_BE_AUTHENTICATED_KEY, true), + ); +} + +export function AuthenticateCondition(func: AuthenticationFunction) { + return applyDecorators( + SetMetadata(CUSTOM_AUTHENTICATION_FUNCTION_KEY, func), + SetMetadata(MUST_BE_AUTHENTICATED_KEY, true), + ); +} + +export function Authenticated() { + return SetMetadata(MUST_BE_AUTHENTICATED_KEY, true); +} diff --git a/src/authentication/authentication.guard.ts b/src/authentication/authentication.guard.ts new file mode 100644 index 0000000..bf4fbf2 --- /dev/null +++ b/src/authentication/authentication.guard.ts @@ -0,0 +1,158 @@ +import { + Injectable, + CanActivate, + OnApplicationBootstrap, + ExecutionContext, + Logger, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import assert from 'assert'; +import jwt from 'jsonwebtoken'; + +import { AuthClientService } from '../clients/auth/client.service'; +import { Permission } from './permissions.enum'; +import { + AuthenticationFunction, + PERMISSIONS_KEY, + CUSTOM_AUTHENTICATION_FUNCTION_KEY, + MUST_BE_AUTHENTICATED_KEY, +} from './authentication.decorator'; +import ErrorBuilder from '../utils/ErrorBuilder'; +import ErrorCodes from '../utils/errorCodes'; + +@Injectable() +export class AuthenticationGuard + implements CanActivate, OnApplicationBootstrap +{ + private readonly logger = new Logger(AuthenticationGuard.name); + + pems: Map; + + constructor( + private reflector: Reflector, + private authClient: AuthClientService, + ) { + this.pems = new Map(); + } + + async onApplicationBootstrap() { + return this.loadDucJWKS(); + } + + async loadDucJWKS() { + const { keys } = await this.authClient.getPublicKeys(); + + keys.forEach((key) => { + this.pems.set(key.kid, key.pem); + }); + } + + canActivate(ctx: ExecutionContext): boolean { + const requiredPermissions = this.reflector.getAllAndOverride( + PERMISSIONS_KEY, + [ctx.getHandler(), ctx.getClass()], + ); + const customAuthenticationFunction = + this.reflector.getAllAndOverride( + CUSTOM_AUTHENTICATION_FUNCTION_KEY, + [ctx.getHandler(), ctx.getClass()], + ); + const mustBeAuthenticated = this.reflector.getAllAndOverride( + MUST_BE_AUTHENTICATED_KEY, + [ctx.getHandler(), ctx.getClass()], + ); + + const request = ctx.switchToHttp().getRequest(); + const accessToken = this.verifyToken(request); + + if (!accessToken) { + // couldn't load valid token + + if (!mustBeAuthenticated) { + // no need to be authenticated + return true; + } + + throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED); + } + + if ( + typeof customAuthenticationFunction === 'function' && + !customAuthenticationFunction(request, request.user) + ) { + // custom authentication function forbidden this request + throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN); + } + + if ( + Array.isArray(requiredPermissions) && + requiredPermissions.length > 0 && + !this.matchPermissions(requiredPermissions, accessToken.permissions) + ) { + // couldn't match permissions + throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN); + } + + return true; + } + + private verifyToken(request) { + const accessToken = request.get('Authorization'); + let accessTokenPayload; + + // If the user isn't authenticated, an error will occurr anywhere here. + // Fancy error avoidance isn't performed by purpose, such as avoiding to access null values. + try { + const accessTokenDecoded: any = jwt.decode(accessToken, { + complete: true, + }); + + /*assert( + accessTokenDecoded.payload.iss === 'duc', + 'token should be issued by DUC', + );*/ + + assert( + accessTokenDecoded.payload.token_use === 'access', + 'should be an access token', + ); + + const { kid } = accessTokenDecoded.header; + const pemValue: string = this.pems.get(kid); + + jwt.verify(accessToken, pemValue); + accessTokenPayload = accessTokenDecoded.payload; + } catch (err) { + this.logger.log('Unable to verify duc access token\n' + err.stack); + return false; + } + + request.accessTokenPayload = accessTokenPayload; + request.user = { + user_id: accessTokenPayload.user_id, + username: accessTokenPayload.username, + permissions: accessTokenPayload.permissions, + customer_id: accessTokenPayload.customer_id, + customer_name: accessTokenPayload.customer_name, + customer_tier: accessTokenPayload.customer_tier, + }; + // TODO: for backwards compatibility. remove in the future + request.body.info = { + user_id: accessTokenPayload.user_id, + customer_id: accessTokenPayload.customer_id, + customer: accessTokenPayload.customer_name, + customer_tier: accessTokenPayload.customer_tier, + }; + + return accessTokenPayload; + } + + private matchPermissions( + requiredPermissions: Permission[], + userPermissions: number[], + ) { + return requiredPermissions.some((permission) => + userPermissions.includes(permission.seqid), + ); + } +} diff --git a/src/authentication/permissions.enum.ts b/src/authentication/permissions.enum.ts new file mode 100644 index 0000000..0e863cf --- /dev/null +++ b/src/authentication/permissions.enum.ts @@ -0,0 +1,216 @@ +import assert from 'assert'; + +export enum PermissionUsages { + PUBLIC = 'public', + INTERNAL = 'internal', +} + +export interface Permission { + seqid: number; + claim: string; + usage: PermissionUsages; +} + +// like field numbers in gRPC, avoid to change (or reuse previously used) seqids once its +// deployed to DUC +/*export const Permissions: { + [P in keyof any]: { [Q in keyof any]: Permission }; +} = {*/ +export const Permissions = { + AUTH: { + CREATE: { + seqid: 18, + claim: 'POST /auth', + usage: PermissionUsages.PUBLIC, + }, + }, + + PIPELINE: { + CREATE: { + seqid: 23, + claim: 'POST /pipelines', + usage: PermissionUsages.PUBLIC, + }, + GET: { + seqid: 13, + claim: 'GET /pipelines', + usage: PermissionUsages.PUBLIC, + }, + UPDATE: { + seqid: 29, + claim: 'PUT /pipelines', + usage: PermissionUsages.PUBLIC, + }, + DELETE: { + seqid: 5, + claim: 'DELETE /pipelines', + usage: PermissionUsages.PUBLIC, + }, + }, + + INPUT: { + CREATE: { + seqid: 21, + claim: 'POST /inputs', + usage: PermissionUsages.PUBLIC, + }, + GET: { + seqid: 9, + claim: 'GET /inputs', + usage: PermissionUsages.PUBLIC, + }, + UPDATE: { + seqid: 27, + claim: 'PUT /inputs', + usage: PermissionUsages.PUBLIC, + }, + UPDATE_PARTIAL: { + seqid: 17, + claim: 'PATCH /inputs', + usage: PermissionUsages.PUBLIC, + }, + DELETE: { + seqid: 3, + claim: 'DELETE /inputs', + usage: PermissionUsages.PUBLIC, + }, + GET_ENTITIES: { + seqid: 10, + claim: 'GET /inputs/available-entities', + usage: PermissionUsages.PUBLIC, + }, + }, + + OUTPUT: { + CREATE: { + seqid: 22, + claim: 'POST /outputs', + usage: PermissionUsages.PUBLIC, + }, + GET: { + seqid: 12, + claim: 'GET /outputs', + usage: PermissionUsages.PUBLIC, + }, + UPDATE: { + seqid: 28, + claim: 'PUT /outputs', + usage: PermissionUsages.PUBLIC, + }, + DELETE: { + seqid: 4, + claim: 'DELETE /outputs', + usage: PermissionUsages.PUBLIC, + }, + }, + + TRANSFORMATIONS: { + CREATE: { + seqid: 24, + claim: 'POST /transformations', + usage: PermissionUsages.PUBLIC, + }, + GET: { + seqid: 15, + claim: 'GET /transformations', + usage: PermissionUsages.PUBLIC, + }, + UPDATE: { + seqid: 30, + claim: 'PUT /transformations', + usage: PermissionUsages.PUBLIC, + }, + DELETE: { + seqid: 6, + claim: 'DELETE /transformations', + usage: PermissionUsages.PUBLIC, + }, + }, + + CATALOG: { + CREATE: { + seqid: 19, + claim: 'POST /catalog', + usage: PermissionUsages.PUBLIC, + }, + GET: { + seqid: 7, + claim: 'GET /catalog', + usage: PermissionUsages.PUBLIC, + }, + UPDATE: { + seqid: 25, + claim: 'PUT /catalog', + usage: PermissionUsages.PUBLIC, + }, + DELETE: { + seqid: 1, + claim: 'DELETE /catalog', + usage: PermissionUsages.PUBLIC, + }, + }, + + CONNECTORS: { + CREATE: { + seqid: 20, + claim: 'POST /connectors', + usage: PermissionUsages.PUBLIC, + }, + GET: { + seqid: 8, + claim: 'GET /connectors', + usage: PermissionUsages.PUBLIC, + }, + UPDATE: { + seqid: 26, + claim: 'PUT /connectors', + usage: PermissionUsages.PUBLIC, + }, + DELETE: { + seqid: 2, + claim: 'DELETE /connectors', + usage: PermissionUsages.PUBLIC, + }, + }, + + SNOWFLAKE: { + OPEN: { + seqid: 14, + claim: 'GET /snowflake', + usage: PermissionUsages.PUBLIC, + }, + }, + + ZENDESK: { + OPEN: { + seqid: 16, + claim: 'GET /zendesk', + usage: PermissionUsages.PUBLIC, + }, + }, + + METABASE: { + OPEN: { + seqid: 11, + claim: 'GET /metabase', + usage: PermissionUsages.PUBLIC, + }, + }, +}; + +// traverses the object searching for duplicate seqids or claims (executes at runtime) +const seqids = Object.values(Permissions).flatMap((namespace) => + Object.values(namespace).map(({ seqid }) => seqid), +); +const claims = Object.values(Permissions).flatMap((namespace) => + Object.values(namespace).map(({ claim }) => claim), +); +Object.values(Permissions).map((namespace) => + Object.values(namespace).map(({ seqid, claim }) => { + const seqidsCount = seqids.filter((x) => x === seqid).length; + assert(seqidsCount === 1, `seqid ${seqid} count is not 1`); + + const claimsCount = claims.filter((x) => x === claim).length; + assert(claimsCount === 1, `claim '${claim}' count is not 1`); + }), +); diff --git a/src/authentication/user.decorator.ts b/src/authentication/user.decorator.ts new file mode 100644 index 0000000..d5fe984 --- /dev/null +++ b/src/authentication/user.decorator.ts @@ -0,0 +1,23 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +import ErrorBuilder from '../utils/ErrorBuilder'; +import ErrorCodes from '../utils/errorCodes'; + +export interface RequestUser { + user_id: string; + username: string; + permissions: string; + customer_id: string; + customer_name: string; + customer_tier: string; +} + +export const User = createParamDecorator((data: any, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + + if (!request.user && data?.required) { + throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED); + } + + return request.user; +}); diff --git a/src/clients/auth/client.service.ts b/src/clients/auth/client.service.ts index 840851a..15a1faa 100644 --- a/src/clients/auth/client.service.ts +++ b/src/clients/auth/client.service.ts @@ -1,259 +1,150 @@ -import { OnModuleInit, Inject } from '@nestjs/common'; +import { Logger, OnModuleInit, Inject } from '@nestjs/common'; import { ClientGrpc } from '@nestjs/microservices'; +import { ProtoServices } from '@victorradael/protospack-v2/dist/lib/Duc'; +import { AuthProtoService as AuthServiceInterface } from '@victorradael/protospack-v2/dist/lib/Duc/interfaces/write-service'; import { - DucServicesNames, - AuthServiceInterface, + AuthGetPublicKeysResponse, AuthSignInRequest, + AuthSignInResponse, AuthRefreshAccessTokenRequest, - AuthConfirmationRequest, - AuthResendConfirmationCodeRequest, + AuthRefreshAccessTokenResponse, AuthEnableTotpMfaRequest, + AuthEnableTotpMfaResponse, AuthDisableTotpMfaRequest, + AuthDisableTotpMfaResponse, AuthDismissTotpMfaRequest, + AuthDismissTotpMfaResponse, AuthVerifyTotpMfaRequest, + AuthVerifyTotpMfaResponse, AuthChangePasswordRequest, + AuthChangePasswordResponse, AuthResetPasswordRequest, + AuthResetPasswordResponse, AuthVerifyResetPasswordCodeRequest, + AuthVerifyResetPasswordCodeResponse, AuthConfirmResetPasswordRequest, -} from '@victorradael/protospack'; + AuthConfirmResetPasswordResponse, +} from '@victorradael/protospack-v2/dist/lib/Duc/interfaces/messages'; + +import grpcHandler from '../../utils/grpcHandler'; export class AuthClientService implements OnModuleInit { + private readonly logger = new Logger(AuthClientService.name); + private authService: AuthServiceInterface; - constructor( - @Inject('AUTH_PACKAGE') private readonly grpcClient: ClientGrpc, - ) {} + constructor(@Inject('DUC_PACKAGE') private readonly grpcClient: ClientGrpc) {} onModuleInit() { this.authService = this.grpcClient.getService( - DucServicesNames.AuthProtoService, + ProtoServices.AuthProtoService, ); } - async signIn({ username, password, totp }: AuthSignInRequest): Promise { - console.log('AuthClientService', 'SignIn'); + async getPublicKeys() { + this.logger.log('GetPublicKeys'); - return new Promise((resolve, reject) => { - this.authService.signIn({ username, password, totp }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); + return grpcHandler( + this.authService.AuthGetPublicKeys({}), + ); } - async enableTotpMFA({ - accessToken, - password, - }: AuthEnableTotpMfaRequest): Promise { - console.log('AuthClientService', 'enableTotpMFA'); + async signIn({ username, password, totp }: AuthSignInRequest) { + this.logger.log('SignIn'); - return new Promise((resolve, reject) => { - this.authService.enableTotpMfa({ accessToken, password }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); + return grpcHandler( + this.authService.AuthSignIn({ username, password, totp }), + ); } - async disableTotpMFA({ - accessToken, - password, - }: AuthDisableTotpMfaRequest): Promise { - console.log('AuthClientService', 'disableTotpMFA'); + async refreshAccessToken({ refreshToken }: AuthRefreshAccessTokenRequest) { + this.logger.log('RefreshAccessToken'); - return new Promise((resolve, reject) => { - this.authService.disableTotpMfa({ accessToken, password }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); - } - - async dismissTotpMFA({ - accessToken, - }: AuthDismissTotpMfaRequest): Promise { - console.log('AuthClientService', 'dismissTotpMFA'); - - return new Promise((resolve, reject) => { - this.authService.dismissTotpMfa({ accessToken }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); - } - - async verifyTotp({ - accessToken, - totp, - }: AuthVerifyTotpMfaRequest): Promise { - console.log('AuthClientService', 'disableTotpMFA'); - - return new Promise((resolve, reject) => { - this.authService.verifyTotp({ accessToken, totp }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); - } - - async confirmRegister({ - username, - code, - }: AuthConfirmationRequest): Promise { - console.log('AuthClientService', 'confirmRegister'); - - const tokens = await new Promise((resolve, reject) => { - this.authService.confirmRegister({ username, code }).subscribe({ - next(x) { - resolve(x); - }, - error(err) { - console.log('Observable Error'); - reject(err); - }, - complete() { - console.log('done'); - }, - }); - }) - .then((res) => res) - .catch((err) => { - throw new Error(err); - }); - return tokens; - } - - async resendeConfirmationCode({ - username, - }: AuthResendConfirmationCodeRequest): Promise { - console.log('AuthClientService', 'resendConfirmationCode'); - - const authResendConfirmationCodeRequestReturn = await new Promise( - (resolve, reject) => { - this.authService.resendConfirmationCode({ username }).subscribe({ - next(x) { - resolve(x); - }, - error(err) { - console.log('Observable Error'); - reject(err); - }, - complete() { - console.log('done'); - }, - }); - }, - ) - .then((res) => res) - .catch((err) => { - throw new Error(err); - }); - return authResendConfirmationCodeRequestReturn; - } - - async refreshAccessToken({ - refreshToken, - }: AuthRefreshAccessTokenRequest): Promise { - console.log('AuthClientService', 'RefreshAccessToken'); - - return new Promise((resolve, reject) => { - this.authService.refreshAccessToken({ refreshToken }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); + return grpcHandler( + this.authService.AuthRefreshAccessToken({ refreshToken }), + ); } async changePassword({ accessToken, oldPassword, newPassword, - }: AuthChangePasswordRequest): Promise { - console.log('AuthClientService', 'ChangePassword'); + }: AuthChangePasswordRequest) { + this.logger.log('ChangePassword'); - return new Promise((resolve, reject) => { - this.authService - .changePassword({ - accessToken, - oldPassword, - newPassword, - }) - .subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); + return grpcHandler( + this.authService.AuthChangePassword({ + accessToken, + oldPassword, + newPassword, + }), + ); } - async resetPassword({ username }: AuthResetPasswordRequest): Promise { - console.log('AuthClientService', 'resetPassword'); + async resetPassword({ username }: AuthResetPasswordRequest) { + this.logger.log('resetPassword'); - return new Promise((resolve, reject) => { - this.authService.resetPassword({ username }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); + return grpcHandler( + this.authService.AuthResetPassword({ username }), + ); } async verifyResetPasswordCode({ username, code, - }: AuthVerifyResetPasswordCodeRequest): Promise { - console.log('AuthClientService', 'verifyResetPasswordCode'); + }: AuthVerifyResetPasswordCodeRequest) { + this.logger.log('verifyResetPasswordCode'); - return new Promise((resolve, reject) => { - this.authService.verifyResetPasswordCode({ username, code }).subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); + return grpcHandler( + this.authService.AuthVerifyResetPasswordCode({ username, code }), + ); } async confirmResetPassword({ username, code, newPassword, - }: AuthConfirmResetPasswordRequest): Promise { - console.log('AuthClientService', 'confirmResetPassword'); + }: AuthConfirmResetPasswordRequest) { + this.logger.log('confirmResetPassword'); - return new Promise((resolve, reject) => { - this.authService - .confirmResetPassword({ username, code, newPassword }) - .subscribe({ - next: resolve, - error: (err) => reject(err.details), - complete() { - console.log('done'); - }, - }); - }); + return grpcHandler( + this.authService.AuthConfirmResetPassword({ + username, + code, + newPassword, + }), + ); + } + + async enableTotpMFA({ accessToken, password }: AuthEnableTotpMfaRequest) { + this.logger.log('enableTotpMFA'); + + return grpcHandler( + this.authService.AuthEnableTotpMfa({ accessToken, password }), + ); + } + + async disableTotpMFA({ accessToken, password }: AuthDisableTotpMfaRequest) { + this.logger.log('disableTotpMFA'); + + return grpcHandler( + this.authService.AuthDisableTotpMfa({ accessToken, password }), + ); + } + + async dismissTotpMFA({ accessToken }: AuthDismissTotpMfaRequest) { + this.logger.log('dismissTotpMFA'); + + return grpcHandler( + this.authService.AuthDismissTotpMfa({ accessToken }), + ); + } + + async verifyTotp({ accessToken, totp }: AuthVerifyTotpMfaRequest) { + this.logger.log('disableTotpMFA'); + + return grpcHandler( + this.authService.AuthVerifyTotpMfa({ accessToken, totp }), + ); } } diff --git a/src/clients/auth/client.config.ts b/src/clients/duc/client.config.ts similarity index 66% rename from src/clients/auth/client.config.ts rename to src/clients/duc/client.config.ts index 82cce15..e1f5de6 100644 --- a/src/clients/auth/client.config.ts +++ b/src/clients/duc/client.config.ts @@ -1,20 +1,23 @@ import { credentials } from '@grpc/grpc-js'; import { ClientOptions, Transport } from '@nestjs/microservices'; +import { + ProtoPackages, + ProtoPaths, +} from '@victorradael/protospack-v2/dist/lib/Duc'; -import { DucProtoFilePath, DucPackages } from '@victorradael/protospack'; - -export class AuthClient { +export class DucClient { config(): ClientOptions { return { transport: Transport.GRPC, options: { url: process.env.DUC_URL, - package: DucPackages, + package: ProtoPackages.WritePackage, credentials: process.env.LOCAL_ENV ? undefined : credentials.createSsl(), - protoPath: DucProtoFilePath, + protoPath: ProtoPaths.WriteFilePath, loader: { + keepCase: true, enums: String, objects: true, arrays: true, diff --git a/src/clients/permissions/client.service.ts b/src/clients/permissions/client.service.ts new file mode 100644 index 0000000..7e9236b --- /dev/null +++ b/src/clients/permissions/client.service.ts @@ -0,0 +1,33 @@ +import { Logger, OnModuleInit, Inject } from '@nestjs/common'; +import { ClientGrpc } from '@nestjs/microservices'; + +import { ProtoServices } from '@victorradael/protospack-v2/dist/lib/Duc'; +import { PermissionsProtoService as PermissionsServiceInterface } from '@victorradael/protospack-v2/dist/lib/Duc/interfaces/write-service'; +import { + Empty, + InjectPermissionsRequest, +} from '@victorradael/protospack-v2/dist/lib/Duc/interfaces/messages'; + +import grpcHandler from '../../utils/grpcHandler'; + +export class PermissionsClientService implements OnModuleInit { + private readonly logger = new Logger(PermissionsClientService.name); + + private permissionsService: PermissionsServiceInterface; + constructor(@Inject('DUC_PACKAGE') private readonly grpcClient: ClientGrpc) {} + + onModuleInit() { + this.permissionsService = + this.grpcClient.getService( + ProtoServices.PermissionsProtoService, + ); + } + + async injectPermissions({ permissions }: InjectPermissionsRequest) { + this.logger.log('InjectPermissions'); + + return grpcHandler( + this.permissionsService.InjectPermissions({ permissions }), + ); + } +} diff --git a/src/middlewares/authentication.ts b/src/middlewares/authentication.ts deleted file mode 100644 index 8cab798..0000000 --- a/src/middlewares/authentication.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import axios from 'axios'; -import { - ForbiddenException, - InternalServerErrorException, - NestMiddleware, - UnauthorizedException, - UseFilters, -} from '@nestjs/common'; -import jwkToPem from 'jwk-to-pem'; -import { decode, verify } from 'jsonwebtoken'; -import { HttpExceptionFilter } from '../error/http-exception.filter'; - -@UseFilters(new HttpExceptionFilter()) -export class LoggerMiddleware implements NestMiddleware { - use = async (request: Request, response: Response, next: NextFunction) => { - const idToken = request.get('Dadosfera-User'); - const accessToken = request.get('Authorization'); - const privateKey = process.env.JWT_PRIVATE_KEY; - let requiredRoute = ''; - const requiredMethod = request.method.trim(); - if (request.route.path.split('/').length >= 2) { - requiredRoute = request.route.path.split('/')[1].trim(); - } - - verify(idToken, privateKey, (err) => { - if (err) { - throw new UnauthorizedException(); - } - }); - const jwtDecoded: any = decode(idToken); - - const permissions = jwtDecoded.user.permissions; - - const clientId = jwtDecoded.user.customerId; - const customer = jwtDecoded.user.customer; - const userId = jwtDecoded.user.id; - const customer_tier = jwtDecoded.user.customer_tier; - - await verifyToken(accessToken); - - let havePermission = false; - const permission = permissions.find((permission) => { - permission = permission.split('/'); - const method = permission[0].trim(); - const route = permission[1].trim(); - - if (method === requiredMethod && route === requiredRoute) { - return permission; - } - }); - - if (permission) { - havePermission = true; - } - - if (havePermission) { - request.body.info = { - customer_id: clientId, - user_id: userId, - customer, - customer_tier, - }; - next(); - } else { - throw new ForbiddenException(); - } - }; -} - -let pems: { [key: string]: Record }[]; - -const setUp = async (region: string, id: string) => { - const URL = `https://cognito-idp.${region}.amazonaws.com/${id}/.well-known/jwks.json`; - - try { - const response = await axios.get(URL); - - if (response.status !== 200) { - throw new InternalServerErrorException(); - } - const data = await response.data; - const { keys } = data; - - pems = keys.map((key: any) => { - const modulus = key.n; - const exponent = key.e; - const keyType = key.kty; - const jwk = { kty: keyType, n: modulus, e: exponent }; - const pem = jwkToPem(jwk); - const keyId = key.kid; - - return { [keyId]: pem }; - }); - } catch (error) { - // console.log(error); - // console.log('Error! Unable to download JWKs'); - } -}; - -const verifyToken = async (accessToken: string) => { - const awsRegion = process.env.AWS_REGION; - const awsPoolId = process.env.AWS_IDENTITY_POOL_ID; - - try { - await setUp(awsRegion, awsPoolId); - if (!accessToken) { - throw new UnauthorizedException(); - } - - const user: any = decode(accessToken, { complete: true }); - - if (user === null) { - throw new UnauthorizedException(); - } - - const { kid } = user.header; - const pem = pems.filter((item: any) => item[kid]); - const pemValue: any = pem[0][kid]; - - if (!pem) { - throw new UnauthorizedException(); - } - - verify(accessToken, pemValue, (err: any) => { - if (err) { - throw new UnauthorizedException(); - } - - return; - }); - } catch (error) { - throw new UnauthorizedException(); - } -}; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index acaab27..62709eb 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,60 +1,72 @@ import { + Logger, Body, Controller, Headers, Post, HttpCode, HttpStatus, + OnApplicationBootstrap, } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { AuthSignInRequest, AuthRefreshAccessTokenRequest, - AuthEnableTotpMfaRequest, - AuthDisableTotpMfaRequest, - AuthVerifyTotpMfaRequest, AuthChangePasswordRequest, AuthResetPasswordRequest, AuthVerifyResetPasswordCodeRequest, AuthConfirmResetPasswordRequest, -} from '@victorradael/protospack'; + AuthEnableTotpMfaRequest, + AuthDisableTotpMfaRequest, + AuthVerifyTotpMfaRequest, +} from '@victorradael/protospack-v2/dist/lib/Duc/interfaces/messages'; -import { AuthClientService } from 'src/clients/auth/client.service'; +import { AuthClientService } from '../../clients/auth/client.service'; +import { PermissionsClientService } from '../../clients/permissions/client.service'; +import { Permissions } from '../../authentication/permissions.enum'; import ErrorBuilder from '../../utils/ErrorBuilder'; +@ApiTags('Auth') @Controller('auth') -export class AuthController { - constructor(private authClient: AuthClientService) {} +export class AuthController implements OnApplicationBootstrap { + private readonly logger = new Logger(AuthController.name); - // DEPRECADO! - @Post() - @HttpCode(HttpStatus.OK) - async signIn_old(@Body() body: AuthSignInRequest) { - console.log(`/auth`, 'SignIn_old'); + constructor( + private authClient: AuthClientService, + private permissionsClient: PermissionsClientService, + ) {} - return this.signIn(body); - } + // internally used to send permissions to duc on microservice startup + async onApplicationBootstrap() { + this.logger.log('sending permissions to DUC...'); - @Post('login') - @HttpCode(HttpStatus.OK) - async signIn_login(@Body() body: AuthSignInRequest) { - console.log(`/auth`, 'SignIn_login'); + const permissions = Object.values(Permissions).flatMap((namespace) => + Object.values(namespace), + ); - return this.signIn(body); + return this.permissionsClient + .injectPermissions({ permissions }) + .catch((err: ErrorBuilder) => { + if ( + err.code === 'No connection established' && + process.env.LOCAL_ENV === 'true' + ) { + return this.logger.log( + "couldn't connect to DUC. suppresing in local env", + ); + } + + throw err; + }); } @Post('sign-in') @HttpCode(HttpStatus.OK) async signIn(@Body() { username, password, totp }: AuthSignInRequest) { - console.log(`/auth`, 'SignIn'); + this.logger.log(`/auth`, 'SignIn'); - const tokens = await this.authClient - .signIn({ username, password, totp }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return tokens; + return this.authClient.signIn({ username, password, totp }); } @Post('refresh-access-token') @@ -62,15 +74,63 @@ export class AuthController { async refreshAccessToken( @Body() { refreshToken }: AuthRefreshAccessTokenRequest, ) { - console.log(`/auth`, 'RefreshAccessToken'); + this.logger.log(`/auth`, 'RefreshAccessToken'); - const accessToken = await this.authClient - .refreshAccessToken({ refreshToken }) - .catch((err) => { - throw ErrorBuilder(err); - }); + return this.authClient.refreshAccessToken({ refreshToken }); + } - return accessToken; + @Post('change-password') + @HttpCode(HttpStatus.OK) + async changePassword( + @Body() body: AuthChangePasswordRequest, + @Headers() headers, + ) { + this.logger.log(`/auth`, 'change-password'); + + const { oldPassword, newPassword } = body; + const { authorization: accessToken } = headers; + + return this.authClient.changePassword({ + accessToken, + oldPassword, + newPassword, + }); + } + + @Post('reset-password') + @HttpCode(HttpStatus.OK) + async resetPassword(@Body() body: AuthResetPasswordRequest) { + this.logger.log(`/auth`, 'reset-password'); + + const { username } = body; + + return this.authClient.resetPassword({ username }); + } + + @Post('verify-reset-password-code') + @HttpCode(HttpStatus.OK) + async verifyResetPasswordCode( + @Body() body: AuthVerifyResetPasswordCodeRequest, + ) { + this.logger.log(`/auth`, 'verify-reset-password-code'); + + const { username, code } = body; + + return this.authClient.verifyResetPasswordCode({ username, code }); + } + + @Post('confirm-reset-password') + @HttpCode(HttpStatus.OK) + async confirmResetPassword(@Body() body: AuthConfirmResetPasswordRequest) { + this.logger.log(`/auth`, 'confirm-reset-password'); + + const { username, code, newPassword } = body; + + return this.authClient.confirmResetPassword({ + username, + code, + newPassword, + }); } @Post('enable-totp') @@ -79,18 +139,12 @@ export class AuthController { @Body() body: AuthEnableTotpMfaRequest, @Headers() headers, ) { - console.log(`/auth`, 'enable-totp'); + this.logger.log(`/auth`, 'enable-totp'); const { password } = body; const { authorization: accessToken } = headers; - const response = await this.authClient - .enableTotpMFA({ accessToken, password }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; + return this.authClient.enableTotpMFA({ accessToken, password }); } @Post('disable-totp') @@ -99,129 +153,32 @@ export class AuthController { @Body() body: AuthDisableTotpMfaRequest, @Headers() headers, ) { - console.log(`/auth`, 'disable-totp'); + this.logger.log(`/auth`, 'disable-totp'); const { password } = body; const { authorization: accessToken } = headers; - const response = await this.authClient - .disableTotpMFA({ accessToken, password }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; + return this.authClient.disableTotpMFA({ accessToken, password }); } @Post('dismiss-totp') @HttpCode(HttpStatus.OK) async dismissTotpMFA(@Headers() headers) { - console.log(`/auth`, 'disable-totp'); + this.logger.log(`/auth`, 'disable-totp'); const { authorization: accessToken } = headers; - const response = await this.authClient - .dismissTotpMFA({ accessToken }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; + return this.authClient.dismissTotpMFA({ accessToken }); } @Post('verify-totp') @HttpCode(HttpStatus.OK) - async verifyTotp( - @Body() body: AuthVerifyTotpMfaRequest, - @Headers() headers, - ): Promise { - console.log(`/auth`, 'enable-totp'); + async verifyTotp(@Body() body: AuthVerifyTotpMfaRequest, @Headers() headers) { + this.logger.log(`/auth`, 'enable-totp'); const { totp } = body; const { authorization: accessToken } = headers; - const response = await this.authClient - .verifyTotp({ accessToken, totp }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; - } - - @Post('change-password') - @HttpCode(HttpStatus.OK) - async changePassword( - @Body() body: AuthChangePasswordRequest, - @Headers() headers, - ): Promise { - console.log(`/auth`, 'change-password'); - - const { oldPassword, newPassword } = body; - const { authorization: accessToken } = headers; - - const response = await this.authClient - .changePassword({ - accessToken, - oldPassword, - newPassword, - }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; - } - - @Post('reset-password') - @HttpCode(HttpStatus.OK) - async resetPassword(@Body() body: AuthResetPasswordRequest): Promise { - console.log(`/auth`, 'reset-password'); - - const { username } = body; - - const response = await this.authClient - .resetPassword({ username }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; - } - - @Post('verify-reset-password-code') - @HttpCode(HttpStatus.OK) - async verifyResetPasswordCode( - @Body() body: AuthVerifyResetPasswordCodeRequest, - ): Promise { - console.log(`/auth`, 'verify-reset-password-code'); - - const { username, code } = body; - - const response = await this.authClient - .verifyResetPasswordCode({ username, code }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; - } - - @Post('confirm-reset-password') - @HttpCode(HttpStatus.OK) - async confirmResetPassword( - @Body() body: AuthConfirmResetPasswordRequest, - ): Promise { - console.log(`/auth`, 'confirm-reset-password'); - - const { username, code, newPassword } = body; - - const response = await this.authClient - .confirmResetPassword({ username, code, newPassword }) - .catch((err) => { - throw ErrorBuilder(err); - }); - - return response; + return this.authClient.verifyTotp({ accessToken, totp }); } } diff --git a/src/modules/auth/auth.service.spec.ts b/src/modules/auth/auth.service.spec.ts deleted file mode 100644 index bc390a2..0000000 --- a/src/modules/auth/auth.service.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import nock from 'nock'; -import { LoggerMiddleware } from '../../middlewares/authentication'; -import { NextFunction, Request, Response } from 'express'; -import { ConsoleLogger, UnauthorizedException } from '@nestjs/common'; - -describe('PipelinesGrpcServerService', () => { - // let mockRequest: Partial; - // let mockResponse: Partial; - // const nextFunction: NextFunction = jest.fn(); - - // beforeEach(() => { - // mockRequest = { - // headers: {}, - // body: { - // info: {}, - // }, - // }; - // mockResponse = { - // json: jest.fn(), - // }; - // }); - - it('should be defined', () => { - expect(2 + 2).toBe(4); - }); - - // it('Should be able to pass auth', async () => { - // const awsRegion = process.env.AWS_REGION; - // const awsPoolId = process.env.AWS_IDENTITY_POOL_ID; - // const cognitoRes = { - // keys: [ - // { - // alg: 'RS256', - // e: 'AQAB', - // kid: 'z3VA3+i9JZY3JFDJTNWOap8RY+B7C6mFedaqCuvLvqA=', - // kty: 'RSA', - // n: 'vIGCQDkPA_eQUoaGDUsCyK_Whr76mNW0kZ1uac6ZnyY6hsjUIjvwehqv-ux3cQo4rQZQVFcoh8n9cK5jguz4GVdD972vIxoEdyv32nBFVr5e1PBunKJ2Y32GTR_Hl0XiE0hRe1v6cWuTqiC4qm1NY7tXYL3mI9L6s9ztNbhmG_V44y26PdhL4vRVrJaHOAmCs-77U-QYAC7Llkpmjh-8tG1zt9_FJ237cpBUVOuhD-7Nm32_eB9wddBGfBw0F10ko_KJoU-7683_Kv8k9coJzFUSONId-bfnLOzs8j1L6ZAHipXCR7rpmuRYzXztjp4Wmm2zPUToWLdMEy0Hq1OWmw', - // use: 'sig', - // }, - // { - // alg: 'RS256', - // e: 'AQAB', - // kid: 'u+1W9pi+clp8LaPhZVrv4dXMqRkTRD02YWhbm0NvsUw=', - // kty: 'RSA', - // n: 'noGq1cRMAKJPWahqfC_zWasYovSUycaS1basMfEoh3ePLc9zRgmyfiVKYzLRosHMe1uk0Y5ekCBKnWA8Yl7I84Yt7IIIaE44oJjSEGBKT3m8i8YaXzawaNs63KPkRh8553o3KzL75bQWcI_ABKqkf-uAKSCl_XotBGkzLUl4hIOYtAtRGEfKaNMPqyTCT5Zn71pMd0isppaUiTW2T5QLsZV1IBp46aSrl_D5Q_FTsJT7feobQVoHp2zfIorCpkfTXBTBMQTEBFSDyPbc6cULl48VKxp0B0GEwR_kYCEHEVzf41LQcUWZUE0OdBychijkSc9MZJnBWUYQZyedJILWnQ', - // use: 'sig', - // }, - // ], - // }; - - // nock( - // `https://cognito-idp.${awsRegion}.amazonaws.com/${awsPoolId}/.well-known/jwks.json`, - // ) - // .persist() - // .get('') - // .reply(200, cognitoRes); - - // mockRequest.headers['Dadosfera-User'] = - // 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoiZTljYjI1ZGEtMTQyOC00NDJjLTg5NjItYTdkNmQxNjhkODUyIiwibmFtZSI6InJvZHJpZ28uemFtYm9uaSIsInVzZXJuYW1lIjoicm9kcmlnby56YW1ib25pQGRhZG9zZmVyYS5haSIsInJ1bGVJZCI6IjIxNzdmZjg4LThmY2ItNDRlNS1hYzYxLWNjYmU2Y2M3MGU4ZCIsImN1c3RvbWVySWQiOiIxMTI5ODBhMy0wYTEyLTQxYzktYmZmZC03OTFjZjdjYTg3YTIiLCJtZmFTdGF0dXMiOiJwZW5kaW5nIiwiY3JlYXRlZEF0IjoiMjAyMi0wNS0xM1QxNTowOTowOS4xMzhaIiwidXBkYXRlZEF0IjoiMjAyMi0wNS0xM1QxNTowOTowOS4xMzhaIiwicGVybWlzc2lvbnMiOlsiUE9TVCAvcGlwZWxpbmVzIiwiR0VUIC9waXBlbGluZXMiLCJQVVQgL3BpcGVsaW5lcyIsIkRFTEVURSAvcGlwZWxpbmVzIiwiUE9TVCAvaW5wdXRzIiwiR0VUIC9pbnB1dHMiLCJQVVQgL2lucHV0cyIsIkRFTEVURSAvaW5wdXRzIiwiUE9TVCAvb3V0cHV0cyIsIkdFVCAvb3V0cHV0cyIsIlBVVCAvb3V0cHV0cyIsIkRFTEVURSAvb3V0cHV0cyIsIlBPU1QgL3RyYW5zZm9ybWF0aW9ucyIsIkdFVCAvdHJhbnNmb3JtYXRpb25zIiwiUFVUIC90cmFuc2Zvcm1hdGlvbnMiLCJERUxFVEUgL3RyYW5zZm9ybWF0aW9ucyIsIlBPU1QgL2NhdGFsb2ciLCJHRVQgL2NhdGFsb2ciLCJQVVQgL2NhdGFsb2ciLCJERUxFVEUgL2NhdGFsb2ciLCJQT1NUIC9hdXRoIiwiR0VUIC9tZXRhYmFzZSIsIkdFVCAvc25vd2ZsYWtlIl0sImN1c3RvbWVyIjoiZGFkb3NmZXJhIn0sImlhdCI6MTY1MzY1NjE2NywiZXhwIjoxNjUzNzQyNTY3fQ.eeNEEUk_95KOxM-objS7gXz-SCVkNFYpEP688MfLkdI'; - // mockRequest.headers['Authorization'] = - // 'eyJraWQiOiJ1KzFXOXBpK2NscDhMYVBoWlZydjRkWE1xUmtUUkQwMllXaGJtME52c1V3PSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxYjkyZmFkMC1iNWVlLTQwMzAtOGRmYy1hZWM0MGQzYzdkYmUiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV9OVXY3WTJTeGoiLCJjbGllbnRfaWQiOiI0N3RrY3Jxc2NuajY3bWhnYmg3dXQ1YnJzNCIsIm9yaWdpbl9qdGkiOiJhZWJjNjA5NC1hYTJmLTRiZmUtOWExZi01ZWVhYTVmYzAwNDAiLCJldmVudF9pZCI6IjBhY2NjMzJiLTllNzktNGY2NS1iMDFiLWZhZGJlYzFhZWFiNiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE2NTM2NTYxNjcsImV4cCI6MTY1MzY1Nzk2NywiaWF0IjoxNjUzNjU2MTY3LCJqdGkiOiIyYTgzNDhmOC0xNDhlLTQ3NjctODcxYi00M2UyOGRjNjc2NTkiLCJ1c2VybmFtZSI6InJvZHJpZ28uemFtYm9uaUBkYWRvc2ZlcmEuYWkifQ.UgIgeGEH2olNgqPly8cymNgWIZJz5n9N4yeKgTusfGsu5h-TWP_jVhPoCvFoixO2XKzFjSCvKwlKQYZyB74jLQLs-j5C5KGBdOea1HX7pUnEfEVtBINd1kcHib5YpGYq3MHG1uVx8vNJS3XviwgYg4rgNWA4du-QbhMicdRyHolYM-dWxuxtkwTRWMe1Vw6KlTctFsvUWx1GMgjp37tsLONcp3B8OsYXfiR764K_pBNs5gMhJ39gJ7NHHLWkJrtrUTpIoHWaZS_HEgvVyQiJENQvK_bPDMPm_lyF1dW-JBgot4TpAxMmvV1XGabkzycIcAOwUaPsKBlMphgunz1Ffw'; - // mockRequest.method = 'GET'; - // mockRequest.route = { - // path: '/inputs/', - // stack: [ - // { - // method: 'get', - // }, - // ], - // methods: { - // get: true, - // }, - // }; - // const authMiddleware = new LoggerMiddleware(); - - // await authMiddleware.useTest( - // mockRequest as Request, - // mockResponse as Response, - // nextFunction, - // ); - // expect(nextFunction).toHaveBeenCalled(); - // }); - - // it('Should not be able to pass auth', async () => { - // const awsRegion = process.env.AWS_REGION; - // const awsPoolId = process.env.AWS_IDENTITY_POOL_ID; - // const cognitoRes = { - // keys: [ - // { - // alg: 'RS256', - // e: 'AQAB', - // kid: 'z3VA3+i9JZY3JFDJTNWOap8RY+B7C6mFedaqCuvLvqA=', - // kty: 'RSA', - // n: 'vIGCQDkPA_eQUoaGDUsCyK_Whr76mNW0kZ1uac6ZnyY6hsjUIjvwehqv-ux3cQo4rQZQVFcoh8n9cK5jguz4GVdD972vIxoEdyv32nBFVr5e1PBunKJ2Y32GTR_Hl0XiE0hRe1v6cWuTqiC4qm1NY7tXYL3mI9L6s9ztNbhmG_V44y26PdhL4vRVrJaHOAmCs-77U-QYAC7Llkpmjh-8tG1zt9_FJ237cpBUVOuhD-7Nm32_eB9wddBGfBw0F10ko_KJoU-7683_Kv8k9coJzFUSONId-bfnLOzs8j1L6ZAHipXCR7rpmuRYzXztjp4Wmm2zPUToWLdMEy0Hq1OWmw', - // use: 'sig', - // }, - // { - // alg: 'RS256', - // e: 'AQAB', - // kid: 'u+1W9pi+clp8LaPhZVrv4dXMqRkTRD02YWhbm0NvsUw=', - // kty: 'RSA', - // n: 'noGq1cRMAKJPWahqfC_zWasYovSUycaS1basMfEoh3ePLc9zRgmyfiVKYzLRosHMe1uk0Y5ekCBKnWA8Yl7I84Yt7IIIaE44oJjSEGBKT3m8i8YaXzawaNs63KPkRh8553o3KzL75bQWcI_ABKqkf-uAKSCl_XotBGkzLUl4hIOYtAtRGEfKaNMPqyTCT5Zn71pMd0isppaUiTW2T5QLsZV1IBp46aSrl_D5Q_FTsJT7feobQVoHp2zfIorCpkfTXBTBMQTEBFSDyPbc6cULl48VKxp0B0GEwR_kYCEHEVzf41LQcUWZUE0OdBychijkSc9MZJnBWUYQZyedJILWnQ', - // use: 'sig', - // }, - // ], - // }; - - // nock( - // `https://cognito-idp.${awsRegion}.amazonaws.com/${awsPoolId}/.well-known/jwks.json`, - // ) - // .persist() - // .get('') - // .reply(200, cognitoRes); - - // mockRequest.headers['Dadosfera-User'] = - // 'asdeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoiZTljYjI1ZGEtMTQyOC00NDJjLTg5NjItYTdkNmQxNjhkODUyIiwibmFtZSI6InJvZHJpZ28uemFtYm9uaSIsInVzZXJuYW1lIjoicm9kcmlnby56YW1ib25pQGRhZG9zZmVyYS5haSIsInJ1bGVJZCI6IjIxNzdmZjg4LThmY2ItNDRlNS1hYzYxLWNjYmU2Y2M3MGU4ZCIsImN1c3RvbWVySWQiOiIxMTI5ODBhMy0wYTEyLTQxYzktYmZmZC03OTFjZjdjYTg3YTIiLCJtZmFTdGF0dXMiOiJwZW5kaW5nIiwiY3JlYXRlZEF0IjoiMjAyMi0wNS0xM1QxNTowOTowOS4xMzhaIiwidXBkYXRlZEF0IjoiMjAyMi0wNS0xM1QxNTowOTowOS4xMzhaIiwicGVybWlzc2lvbnMiOlsiUE9TVCAvcGlwZWxpbmVzIiwiR0VUIC9waXBlbGluZXMiLCJQVVQgL3BpcGVsaW5lcyIsIkRFTEVURSAvcGlwZWxpbmVzIiwiUE9TVCAvaW5wdXRzIiwiR0VUIC9pbnB1dHMiLCJQVVQgL2lucHV0cyIsIkRFTEVURSAvaW5wdXRzIiwiUE9TVCAvb3V0cHV0cyIsIkdFVCAvb3V0cHV0cyIsIlBVVCAvb3V0cHV0cyIsIkRFTEVURSAvb3V0cHV0cyIsIlBPU1QgL3RyYW5zZm9ybWF0aW9ucyIsIkdFVCAvdHJhbnNmb3JtYXRpb25zIiwiUFVUIC90cmFuc2Zvcm1hdGlvbnMiLCJERUxFVEUgL3RyYW5zZm9ybWF0aW9ucyIsIlBPU1QgL2NhdGFsb2ciLCJHRVQgL2NhdGFsb2ciLCJQVVQgL2NhdGFsb2ciLCJERUxFVEUgL2NhdGFsb2ciLCJQT1NUIC9hdXRoIiwiR0VUIC9tZXRhYmFzZSIsIkdFVCAvc25vd2ZsYWtlIl0sImN1c3RvbWVyIjoiZGFkb3NmZXJhIn0sImlhdCI6MTY1MzY1NjE2NywiZXhwIjoxNjUzNzQyNTY3fQ.eeNEEUk_95KOxM-objS7gXz-SCVkNFYpEP688MfLkdI'; - // mockRequest.headers['Authorization'] = - // 'asdeyJraWQiOiJ1KzFXOXBpK2NscDhMYVBoWlZydjRkWE1xUmtUUkQwMllXaGJtME52c1V3PSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIxYjkyZmFkMC1iNWVlLTQwMzAtOGRmYy1hZWM0MGQzYzdkYmUiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV9OVXY3WTJTeGoiLCJjbGllbnRfaWQiOiI0N3RrY3Jxc2NuajY3bWhnYmg3dXQ1YnJzNCIsIm9yaWdpbl9qdGkiOiJhZWJjNjA5NC1hYTJmLTRiZmUtOWExZi01ZWVhYTVmYzAwNDAiLCJldmVudF9pZCI6IjBhY2NjMzJiLTllNzktNGY2NS1iMDFiLWZhZGJlYzFhZWFiNiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE2NTM2NTYxNjcsImV4cCI6MTY1MzY1Nzk2NywiaWF0IjoxNjUzNjU2MTY3LCJqdGkiOiIyYTgzNDhmOC0xNDhlLTQ3NjctODcxYi00M2UyOGRjNjc2NTkiLCJ1c2VybmFtZSI6InJvZHJpZ28uemFtYm9uaUBkYWRvc2ZlcmEuYWkifQ.UgIgeGEH2olNgqPly8cymNgWIZJz5n9N4yeKgTusfGsu5h-TWP_jVhPoCvFoixO2XKzFjSCvKwlKQYZyB74jLQLs-j5C5KGBdOea1HX7pUnEfEVtBINd1kcHib5YpGYq3MHG1uVx8vNJS3XviwgYg4rgNWA4du-QbhMicdRyHolYM-dWxuxtkwTRWMe1Vw6KlTctFsvUWx1GMgjp37tsLONcp3B8OsYXfiR764K_pBNs5gMhJ39gJ7NHHLWkJrtrUTpIoHWaZS_HEgvVyQiJENQvK_bPDMPm_lyF1dW-JBgot4TpAxMmvV1XGabkzycIcAOwUaPsKBlMphgunz1Ffw'; - // mockRequest.method = 'GET'; - // mockRequest.route = { - // path: '/inputs/', - // stack: [ - // { - // method: 'get', - // }, - // ], - // methods: { - // get: true, - // }, - // }; - // const authMiddleware = new LoggerMiddleware(); - - // // const t = await authMiddleware.useTest( - // // mockRequest as Request, - // // mockResponse as Response, - // // nextFunction, - // // ); - // // expect(nextFunction).toHaveBeenCalled(); - - // expect(async () => { - // const t = await authMiddleware.useTest( - // mockRequest as Request, - // mockResponse as Response, - // nextFunction, - // ); - // console.log(t); - // }).toThrow('Unauthorized'); - - // expect(1).toBe(2); - // // expect(t).toThrow(UnauthorizedException); - // }); -}); diff --git a/src/modules/catalog/catalog.controller.ts b/src/modules/catalog/catalog.controller.ts index d0c572a..3d94b56 100644 --- a/src/modules/catalog/catalog.controller.ts +++ b/src/modules/catalog/catalog.controller.ts @@ -7,9 +7,31 @@ import { Post, Query, } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { AuthenticateCondition } from '../../authentication/authentication.decorator'; +import { Permissions } from '../../authentication/permissions.enum'; import { CatalogService } from './catalog.service'; +@ApiTags('Catalog') @Controller('catalog') +@AuthenticateCondition((req, user) => { + let action; + + switch (req.method) { + case 'POST': + action = 'CREATE'; + break; + + case 'PUT': + action = 'UPDATE'; + break; + + default: + action = req.method; + } + + return user.permissions.includes(Permissions.CATALOG[action].seqid); +}) export class CatalogController { constructor(private catalogService: CatalogService) {} diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts index 86445fb..1617c9c 100644 --- a/src/modules/health/health.controller.ts +++ b/src/modules/health/health.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { HealthService } from './health.service'; +@ApiTags('Health') @Controller('health') export class HealthController { constructor(private readonly healthService: HealthService) {} diff --git a/src/modules/inputs/inputs.controller.ts b/src/modules/inputs/inputs.controller.ts index 71f4340..4e47326 100644 --- a/src/modules/inputs/inputs.controller.ts +++ b/src/modules/inputs/inputs.controller.ts @@ -10,6 +10,9 @@ import { import { InputsService } from './inputs.service'; import { InputsClientService } from 'src/clients/inputs/client.service'; import { UpdateInputRequest } from 'src/clients/inputs/interfaces'; +import { InputNewCreateRequest } from '@victorradael/protospack'; +import { Permissions } from '../../authentication/permissions.enum'; +import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { CreateInputReq, @@ -24,6 +27,35 @@ import { @ApiTags('inputs') @Controller('inputs') +@AuthenticateCondition((req, user) => { + let action; + + switch (req.method) { + case 'POST': + action = 'CREATE'; + break; + + case 'PUT': + action = 'UPDATE'; + break; + + case 'PATCH': + action = 'UPDATE_PARTIAL'; + break; + + default: + action = req.method; + } + + if ( + `${req.method} ${req.route.path}` === + 'GET /inputs/available-entities/:plugin' + ) { + action = 'GET_ENTITIES'; + } + + return user.permissions.includes(Permissions.INPUT[action].seqid); +}) export class InputsController { inputService: InputsService; constructor(private inputsClientService: InputsClientService) { diff --git a/src/modules/outputs/outputs.controllers.ts b/src/modules/outputs/outputs.controllers.ts index e9f73d3..e3d6597 100644 --- a/src/modules/outputs/outputs.controllers.ts +++ b/src/modules/outputs/outputs.controllers.ts @@ -8,12 +8,34 @@ import { Put, } from '@nestjs/common'; import { Payload } from '@nestjs/microservices'; +import { ApiTags } from '@nestjs/swagger'; import { OutputsClientService } from 'src/clients/outputs/client.service'; +import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; +import { Permissions } from '../../authentication/permissions.enum'; import { OutputsService } from './outputs.service'; +@ApiTags('Outputs') @Controller('outputs') +@AuthenticateCondition((req, user) => { + let action; + + switch (req.method) { + case 'POST': + action = 'CREATE'; + break; + + case 'PUT': + action = 'UPDATE'; + break; + + default: + action = req.method; + } + + return user.permissions.includes(Permissions.OUTPUT[action].seqid); +}) export class OutputsController { - constructor(private outputsClientService: OutputsClientService) {} + constructor(private outputsClientService: OutputsClientService) { } @Post() async create(@Body() createOutputDto) { diff --git a/src/modules/pipelines/pipelines.controller.ts b/src/modules/pipelines/pipelines.controller.ts index 961f2a3..7cbfe8e 100644 --- a/src/modules/pipelines/pipelines.controller.ts +++ b/src/modules/pipelines/pipelines.controller.ts @@ -7,10 +7,32 @@ import { Post, Put, } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { PipelinesClientService } from 'src/clients/pipelines/client.service'; +import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; +import { Permissions } from '../../authentication/permissions.enum'; import { PipelinesService } from './pipelines.service'; +@ApiTags('Pipelines') @Controller('pipelines') +@AuthenticateCondition((req, user) => { + let action; + + switch (req.method) { + case 'POST': + action = 'CREATE'; + break; + + case 'PUT': + action = 'UPDATE'; + break; + + default: + action = req.method; + } + + return user.permissions.includes(Permissions.PIPELINE[action].seqid); +}) export class PipelinesController { constructor(private pipelinesClientService: PipelinesClientService) {} diff --git a/src/modules/transformations/transformations.controller.ts b/src/modules/transformations/transformations.controller.ts index 3234dfc..5caea42 100644 --- a/src/modules/transformations/transformations.controller.ts +++ b/src/modules/transformations/transformations.controller.ts @@ -8,11 +8,33 @@ import { Put, } from '@nestjs/common'; import { Payload } from '@nestjs/microservices'; +import { ApiTags } from '@nestjs/swagger'; import { TransformationsClientService } from 'src/clients/transformations/client.service'; import { IIdRequest } from 'src/clients/transformations/interfaces'; +import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; +import { Permissions } from '../../authentication/permissions.enum'; import { TransformationsService } from './transformations.service'; +@ApiTags('Transformations') @Controller('transformations') +@AuthenticateCondition((req, user) => { + let action; + + switch (req.method) { + case 'POST': + action = 'CREATE'; + break; + + case 'PUT': + action = 'UPDATE'; + break; + + default: + action = req.method; + } + + return user.permissions.includes(Permissions.TRANSFORMATIONS[action].seqid); +}) export class TransformationsController { constructor( private transformationsClientService: TransformationsClientService, diff --git a/src/utils/ErrorBuilder.ts b/src/utils/ErrorBuilder.ts index a95f71b..fd7095d 100644 --- a/src/utils/ErrorBuilder.ts +++ b/src/utils/ErrorBuilder.ts @@ -1,131 +1,145 @@ -import { HttpStatus, HttpException } from '@nestjs/common'; +import { HttpStatus, HttpException, Logger } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; import ErrorCodes from './errorCodes'; -function Builder({ statusCode, message, error, code }) { - return new HttpException({ statusCode, message, error, code }, statusCode); -} - -export default function ErrorBuilder(code: string) { - console.log(code); - +function enrichErrorCode(code: string) { switch (code) { case ErrorCodes.AUTH.WRONG_CREDENTIALS: - return Builder({ + return { statusCode: HttpStatus.UNAUTHORIZED, error: 'Não autenticado', message: 'Usuário ou senha incorretos', code, - }); + }; case ErrorCodes.AUTH.WRONG_PASSWORD_CONFIRMATION: - return Builder({ + return { statusCode: HttpStatus.UNAUTHORIZED, error: 'Senha incorreta', message: 'Confirmação de senha incorreta', code, - }); + }; case ErrorCodes.AUTH.TOTP_NOT_ENABLED: - return Builder({ + return { statusCode: HttpStatus.PRECONDITION_FAILED, error: 'Não permitido', message: 'A autenticação multifator não está habilitada', code, - }); + }; case ErrorCodes.AUTH.TOTP_ALREADY_ENABLED: - return Builder({ + return { statusCode: HttpStatus.PRECONDITION_FAILED, error: 'Não permitido', message: 'A autenticação multifator já está habilitada', code, - }); + }; case ErrorCodes.AUTH.TOTP_ALREADY_DISABLED: - return Builder({ + return { statusCode: HttpStatus.PRECONDITION_FAILED, error: 'Não permitido', message: 'A autenticação multifator já está desabilitada', code, - }); + }; case ErrorCodes.AUTH.TOTP_REQUIRED: - return Builder({ + return { statusCode: HttpStatus.UNAUTHORIZED, error: 'Não autenticado', message: 'Informe o token de autenticação multifator', code, - }); + }; case ErrorCodes.AUTH.CODE_MISMATCH: case ErrorCodes.AUTH.CODE_ALREADY_USED: - return Builder({ + return { statusCode: HttpStatus.UNAUTHORIZED, error: 'Não autenticado', message: 'Token de autenticação multifator inválido', code, - }); + }; case ErrorCodes.AUTH.RESET_PASSWORD_CODE_EXPIRED: - return Builder({ + return { statusCode: HttpStatus.UNAUTHORIZED, error: 'Não permitido', message: 'Token para recuperar senha expirado', code, - }); + }; case ErrorCodes.AUTH.RESET_PASSWORD_CODE_INVALID: - return Builder({ + return { statusCode: HttpStatus.UNAUTHORIZED, error: 'Não permitido', message: 'Token para recuperar senha inválido', code, - }); + }; case ErrorCodes.AUTH.WEAK_NEW_PASSWORD: - return Builder({ + return { statusCode: HttpStatus.BAD_REQUEST, error: 'Não permitido', message: 'Senha muito fraca. Escolha uma senha mais forte', code, - }); + }; case ErrorCodes.RATE_LIMIT: - return Builder({ + return { statusCode: HttpStatus.TOO_MANY_REQUESTS, error: 'Limite excedido', message: 'Você tentou realizar essa operação muitas vezes. Tente novamente mais tarde', code, - }); + }; case ErrorCodes.AUTH.UNAUTHORIZED: - return Builder({ + return { statusCode: HttpStatus.UNAUTHORIZED, error: 'Não autenticado', message: 'É necessário estar logado para realizar essa operação', code, - }); + }; case ErrorCodes.AUTH.FORBIDDEN: - return Builder({ + return { statusCode: HttpStatus.FORBIDDEN, error: 'Não autorizado', message: 'Você não tem permissões suficientes para realizar essa operação', code, - }); + }; case ErrorCodes.INTERNAL: case ErrorCodes.UNKNOWN: default: - return Builder({ + return { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, error: 'Desconhecido', message: 'Erro desconhecido. Tente novamente ou entre em contato com o suporte', code: ErrorCodes.UNKNOWN, - }); + }; + } +} + +const logger = new Logger(); + +export default class ErrorBuilder extends HttpException { + code: string; + + constructor(code: string | RpcException) { + if (typeof code !== 'string') { + logger.log(code.stack); + code = (code as any).details as string; + } else { + logger.log(code); + } + + const { statusCode, message, error, code: rCode } = enrichErrorCode(code); + super({ statusCode, message, error, code: rCode }, statusCode); + this.code = code; } } diff --git a/src/utils/grpcHandler.ts b/src/utils/grpcHandler.ts new file mode 100644 index 0000000..0e7d77b --- /dev/null +++ b/src/utils/grpcHandler.ts @@ -0,0 +1,19 @@ +import { Logger } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; +import { from } from 'rxjs'; + +import ErrorBuilder from './ErrorBuilder'; + +const logger = new Logger(); + +export default async function grpcHandler(method: Promise) { + return new Promise((resolve, reject) => { + from(method).subscribe({ + next: resolve, + error: reject, + complete: () => logger.log('done'), + }); + }).catch((err: RpcException) => { + throw new ErrorBuilder(err); + }); +} diff --git a/swagger.json b/swagger.json index a36331c..2a97b88 100644 --- a/swagger.json +++ b/swagger.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/inputs/available-entities/{plugin}":{"get":{"operationId":"InputsController_getAvailableEntities","parameters":[{"name":"plugin","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAvailableEntitiesRes"}}}}},"tags":["inputs"]}},"/inputs/test-connection":{"post":{"operationId":"InputsController_testConnection","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionRes"}}}}},"tags":["inputs"]}},"/inputs/test-connection/get-columns":{"post":{"operationId":"InputsController_getColumns","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionGetColumnsReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionGetColumnsRes"}}}}},"tags":["inputs"]}},"/inputs":{"post":{"operationId":"InputsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInputReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Input"}}}}},"tags":["inputs"]},"get":{"operationId":"InputsController_findAll","parameters":[],"responses":{"200":{"description":""}},"tags":["inputs"]}},"/inputs/{id}":{"post":{"operationId":"InputsController_reCreate","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInputReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Input"}}}}},"tags":["inputs"]},"get":{"operationId":"InputsController_findOne","parameters":[],"responses":{"200":{"description":""}},"tags":["inputs"]},"patch":{"operationId":"InputsController_update","parameters":[],"responses":{"200":{"description":""}},"tags":["inputs"]},"delete":{"operationId":"InputsController_delete","parameters":[],"responses":{"200":{"description":""}},"tags":["inputs"]}},"/transformations":{"post":{"operationId":"TransformationsController_create","parameters":[],"responses":{"201":{"description":""}}},"get":{"operationId":"TransformationsController_findAll","parameters":[],"responses":{"200":{"description":""}}}},"/transformations/{id}":{"get":{"operationId":"TransformationsController_findOne","parameters":[],"responses":{"200":{"description":""}}},"put":{"operationId":"TransformationsController_update","parameters":[],"responses":{"200":{"description":""}}},"delete":{"operationId":"TransformationsController_delete","parameters":[],"responses":{"200":{"description":""}}}},"/outputs":{"post":{"operationId":"OutputsController_create","parameters":[],"responses":{"201":{"description":""}}},"get":{"operationId":"OutputsController_findAll","parameters":[],"responses":{"200":{"description":""}}}},"/outputs/{id}":{"get":{"operationId":"OutputsController_findOne","parameters":[],"responses":{"200":{"description":""}}},"put":{"operationId":"OutputsController_update","parameters":[],"responses":{"200":{"description":""}}},"delete":{"operationId":"OutputsController_delete","parameters":[],"responses":{"200":{"description":""}}}},"/pipelines/start/{id}":{"post":{"operationId":"PipelinesController_activate","parameters":[],"responses":{"201":{"description":""}}}},"/pipelines/{id}/status":{"get":{"operationId":"PipelinesController_getPipelineStatus","parameters":[],"responses":{"200":{"description":""}}}},"/pipelines/{id}/{details}":{"get":{"operationId":"PipelinesController_getPipelineLogs","parameters":[],"responses":{"200":{"description":""}}}},"/pipelines":{"post":{"operationId":"PipelinesController_create","parameters":[],"responses":{"201":{"description":""}}},"get":{"operationId":"PipelinesController_findAll","parameters":[],"responses":{"200":{"description":""}}}},"/pipelines/{id}":{"get":{"operationId":"PipelinesController_findOne","parameters":[],"responses":{"200":{"description":""}}},"put":{"operationId":"PipelinesController_update","parameters":[],"responses":{"200":{"description":""}}},"delete":{"operationId":"PipelinesController_delete","parameters":[],"responses":{"200":{"description":""}}}},"/auth":{"post":{"operationId":"AuthController_signIn_old","parameters":[],"responses":{"200":{"description":""}}}},"/auth/login":{"post":{"operationId":"AuthController_signIn_login","parameters":[],"responses":{"200":{"description":""}}}},"/auth/sign-in":{"post":{"operationId":"AuthController_signIn","parameters":[],"responses":{"200":{"description":""}}}},"/auth/refresh-access-token":{"post":{"operationId":"AuthController_refreshAccessToken","parameters":[],"responses":{"200":{"description":""}}}},"/auth/enable-totp":{"post":{"operationId":"AuthController_enableTotpMFA","parameters":[],"responses":{"200":{"description":""}}}},"/auth/disable-totp":{"post":{"operationId":"AuthController_disableTotpMFA","parameters":[],"responses":{"200":{"description":""}}}},"/auth/dismiss-totp":{"post":{"operationId":"AuthController_dismissTotpMFA","parameters":[],"responses":{"200":{"description":""}}}},"/auth/verify-totp":{"post":{"operationId":"AuthController_verifyTotp","parameters":[],"responses":{"200":{"description":""}}}},"/auth/change-password":{"post":{"operationId":"AuthController_changePassword","parameters":[],"responses":{"200":{"description":""}}}},"/auth/reset-password":{"post":{"operationId":"AuthController_resetPassword","parameters":[],"responses":{"200":{"description":""}}}},"/auth/verify-reset-password-code":{"post":{"operationId":"AuthController_verifyResetPasswordCode","parameters":[],"responses":{"200":{"description":""}}}},"/auth/confirm-reset-password":{"post":{"operationId":"AuthController_confirmResetPassword","parameters":[],"responses":{"200":{"description":""}}}},"/health":{"get":{"operationId":"HealthController_check","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/all":{"get":{"operationId":"CatalogController_catalogAll","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/data_apps":{"get":{"operationId":"CatalogController_dataAppsAll","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/data_apps/{id}":{"delete":{"operationId":"CatalogController_dataAppsOne","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/dashboard-metabase":{"get":{"operationId":"CatalogController_getAllDashboardMetabase","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/dashboard-metabase/{id}":{"get":{"operationId":"CatalogController_getOneDashboardMetabase","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/table-metadata":{"get":{"operationId":"CatalogController_getAllTableMetadata","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/table-metadata/{id}":{"delete":{"operationId":"CatalogController_deleteOneTableMetadata","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/column-metadata":{"get":{"operationId":"CatalogController_getOneColumnMetadata","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/column-metadata/{id}":{"delete":{"operationId":"CatalogController_deleteOneColumnMetadata","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/data-preview":{"get":{"operationId":"CatalogController_getOneDataPreview","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/data-status":{"get":{"operationId":"CatalogController_createDataStatus","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/data-description":{"get":{"operationId":"CatalogController_getDataDescription","parameters":[],"responses":{"200":{"description":""}}},"post":{"operationId":"CatalogController_createDataDescription","parameters":[],"responses":{"201":{"description":""}}}},"/catalog/data-docs":{"get":{"operationId":"CatalogController_getDataDocs","parameters":[],"responses":{"200":{"description":""}}},"post":{"operationId":"CatalogController_createDataDocs","parameters":[],"responses":{"201":{"description":""}}}},"/catalog/data-rating":{"get":{"operationId":"CatalogController_getDataRating","parameters":[],"responses":{"200":{"description":""}}},"post":{"operationId":"CatalogController_createDataRating","parameters":[],"responses":{"201":{"description":""}}}},"/catalog/summary-rating/{id}":{"get":{"operationId":"CatalogController_getSummaryRating","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/data-comment":{"get":{"operationId":"CatalogController_getDataComment","parameters":[],"responses":{"200":{"description":""}}},"post":{"operationId":"CatalogController_createDataComment","parameters":[],"responses":{"201":{"description":""}}}},"/catalog/data-review":{"get":{"operationId":"CatalogController_getDataReview","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/tags":{"post":{"operationId":"CatalogController_createTags","parameters":[],"responses":{"201":{"description":""}}},"get":{"operationId":"CatalogController_findAllTags","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/tags/{id}":{"delete":{"operationId":"CatalogController_deleteTags","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/table-tags":{"post":{"operationId":"CatalogController_createTableTags","parameters":[],"responses":{"201":{"description":""}}},"get":{"operationId":"CatalogController_getAllTableTags","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/table-tags/{id}":{"delete":{"operationId":"CatalogController_deleteTableTags","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/table-rules":{"get":{"operationId":"CatalogController_getTableRules","parameters":[],"responses":{"200":{"description":""}}}},"/catalog/table-rules/{id}":{"delete":{"operationId":"CatalogController_deleteTableRules","parameters":[],"responses":{"200":{"description":""}}}},"/oauth/hubspot":{"get":{"operationId":"OauthController_oauthHubspot","parameters":[],"responses":{"200":{"description":""}},"tags":["oauth"]}},"/oauth/hubspot/callback":{"get":{"operationId":"OauthController_oauthHubspotCallback","parameters":[],"responses":{"200":{"description":""}},"tags":["oauth"]}}},"info":{"title":"Maestro Grpc Documentation","description":"Documentation for Maestro gateway","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"GetAvailableEntitiesRes":{"type":"object","properties":{"entities":{"type":"array","items":{"type":"string"}}},"required":["entities"]},"CredentialsJdbc":{"type":"object","properties":{"jdbc_user":{"type":"string"},"jdbc_password":{"type":"string"},"database":{"type":"string"},"endpoint":{"type":"string"},"port":{"type":"string"},"engine":{"type":"string"},"schema":{"type":"string"}}},"TestConnectionReq":{"type":"object","properties":{"plugin":{"type":"string"},"credentials":{"$ref":"#/components/schemas/CredentialsJdbc"}},"required":["plugin","credentials"]},"TestConnectionRes":{"type":"object","properties":{"connection_state":{"type":"boolean"},"total_entities":{"type":"number"},"database_tables":{"type":"array","items":{"type":"string"}}},"required":["connection_state","total_entities","database_tables"]},"TestConnectionGetColumnsReq":{"type":"object","properties":{"plugin":{"type":"string"},"tables":{"type":"array","items":{"type":"string"}},"credentials":{"$ref":"#/components/schemas/CredentialsJdbc"},"id":{"type":"string"}},"required":["plugin","tables"]},"TestConnectionGetColumnsRes":{"type":"object","properties":{"tables":{"type":"array","items":{"type":"string"}}},"required":["tables"]},"FileFormatParams":{"type":"object","properties":{"file_format":{"type":"string"},"encoding":{"type":"string"}}},"GoogleAnalyticsClientSecrets":{"type":"object","properties":{"type":{"type":"string"},"project_id":{"type":"string"},"private_key_id":{"type":"string"},"private_key":{"type":"string"},"client_email":{"type":"string"},"client_id":{"type":"string"},"auth_uri":{"type":"string"},"token_uri":{"type":"string"},"auth_provider_x509_cert_url":{"type":"string"},"client_x509_cert_url":{"type":"string"}},"required":["type","project_id","private_key_id","private_key","client_email","client_id","auth_uri","token_uri","auth_provider_x509_cert_url","client_x509_cert_url"]},"Credentials":{"type":"object","properties":{"jdbc_user":{"type":"string"},"jdbc_password":{"type":"string"},"database":{"type":"string"},"endpoint":{"type":"string"},"port":{"type":"string"},"engine":{"type":"string"},"schema":{"type":"string"},"connection_type":{"type":"string"},"client_aws_access_key_id":{"type":"string"},"client_aws_secret_access_key":{"type":"string"},"client_bucket":{"type":"string"},"file_to_extract":{"type":"string"},"file_format_params":{"$ref":"#/components/schemas/FileFormatParams"},"view_id":{"type":"string"},"client_secrets":{"$ref":"#/components/schemas/GoogleAnalyticsClientSecrets"},"start_date":{"type":"string"},"end_date":{"type":"string"},"oauth_code":{"type":"string"}},"required":["connection_type"]},"OauthObject":{"type":"object","properties":{"get_tokens_url":{"type":"string"},"get_tokens_url_params":{"type":"string"},"get_tokens_set_response":{"type":"object"},"content_type":{"type":"string"}}},"InputOptions":{"type":"object","properties":{"oauth":{"$ref":"#/components/schemas/OauthObject"},"skip_select_columns":{"type":"boolean"},"skip_select_entities":{"type":"boolean"},"skip_transformation":{"type":"boolean"}}},"CreateInputReq":{"type":"object","properties":{"id":{"type":"string"},"plugin":{"type":"string"},"category":{"type":"string"},"credentials":{"$ref":"#/components/schemas/Credentials"},"name":{"type":"string"},"options":{"$ref":"#/components/schemas/InputOptions"}},"required":["plugin","category","credentials","name"]},"Input":{"type":"object","properties":{"id":{"type":"string"},"category":{"type":"string"},"plugin":{"type":"string"},"name":{"type":"string"},"cron":{"type":"string"},"credentials":{"$ref":"#/components/schemas/Credentials"},"client_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","category","plugin","name","cron","credentials","client_id","created_at","updated_at"]}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/inputs/available-entities/{plugin}":{"get":{"operationId":"InputsController_getAvailableEntities","parameters":[{"name":"plugin","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAvailableEntitiesRes"}}}}},"tags":["inputs"]}},"/inputs/test-connection":{"post":{"operationId":"InputsController_testConnection","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionRes"}}}},"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["inputs"]}},"/inputs/test-connection/get-columns":{"post":{"operationId":"InputsController_getColumns","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionGetColumnsReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestConnectionGetColumnsRes"}}}},"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["inputs"]}},"/inputs":{"post":{"operationId":"InputsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInputReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Input"}}}},"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["inputs"]},"get":{"operationId":"InputsController_findAll","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["inputs"]}},"/inputs/{id}":{"post":{"operationId":"InputsController_reCreate","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInputReq"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Input"}}}},"201":{"description":""}},"tags":["inputs"]},"get":{"operationId":"InputsController_findOne","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["inputs"]},"patch":{"operationId":"InputsController_update","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["inputs"]},"delete":{"operationId":"InputsController_delete","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["inputs"]}},"/transformations":{"post":{"operationId":"TransformationsController_create","parameters":[],"responses":{"201":{"description":""}},"tags":["Transformations"]},"get":{"operationId":"TransformationsController_findAll","parameters":[],"responses":{"200":{"description":""}},"tags":["Transformations"]}},"/transformations/{id}":{"get":{"operationId":"TransformationsController_findOne","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Transformations"]},"put":{"operationId":"TransformationsController_update","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Transformations"]},"delete":{"operationId":"TransformationsController_delete","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Transformations"]}},"/outputs":{"post":{"operationId":"OutputsController_create","parameters":[],"responses":{"201":{"description":""}},"tags":["Outputs"]},"get":{"operationId":"OutputsController_findAll","parameters":[],"responses":{"200":{"description":""}},"tags":["Outputs"]}},"/outputs/{id}":{"get":{"operationId":"OutputsController_findOne","parameters":[],"responses":{"200":{"description":""}},"tags":["Outputs"]},"put":{"operationId":"OutputsController_update","parameters":[],"responses":{"200":{"description":""}},"tags":["Outputs"]},"delete":{"operationId":"OutputsController_delete","parameters":[],"responses":{"200":{"description":""}},"tags":["Outputs"]}},"/pipelines/start/{id}":{"post":{"operationId":"PipelinesController_activate","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Pipelines"]}},"/pipelines/{id}/status":{"get":{"operationId":"PipelinesController_getPipelineStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Pipelines"]}},"/pipelines/{id}/{details}":{"get":{"operationId":"PipelinesController_getPipelineLogs","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Pipelines"]}},"/pipelines":{"post":{"operationId":"PipelinesController_create","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Pipelines"]},"get":{"operationId":"PipelinesController_findAll","parameters":[],"responses":{"200":{"description":""}},"tags":["Pipelines"]}},"/pipelines/{id}":{"get":{"operationId":"PipelinesController_findOne","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Pipelines"]},"put":{"operationId":"PipelinesController_update","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Pipelines"]},"delete":{"operationId":"PipelinesController_delete","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Pipelines"]}},"/auth/sign-in":{"post":{"operationId":"AuthController_signIn","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/refresh-access-token":{"post":{"operationId":"AuthController_refreshAccessToken","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/change-password":{"post":{"operationId":"AuthController_changePassword","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/reset-password":{"post":{"operationId":"AuthController_resetPassword","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/verify-reset-password-code":{"post":{"operationId":"AuthController_verifyResetPasswordCode","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/confirm-reset-password":{"post":{"operationId":"AuthController_confirmResetPassword","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/enable-totp":{"post":{"operationId":"AuthController_enableTotpMFA","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/disable-totp":{"post":{"operationId":"AuthController_disableTotpMFA","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/dismiss-totp":{"post":{"operationId":"AuthController_dismissTotpMFA","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/auth/verify-totp":{"post":{"operationId":"AuthController_verifyTotp","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Auth"]}},"/health":{"get":{"operationId":"HealthController_check","parameters":[],"responses":{"200":{"description":""}},"tags":["Health"]}},"/catalog/all":{"get":{"operationId":"CatalogController_catalogAll","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data_apps":{"get":{"operationId":"CatalogController_dataAppsAll","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data_apps/{id}":{"delete":{"operationId":"CatalogController_dataAppsOne","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/dashboard-metabase":{"get":{"operationId":"CatalogController_getAllDashboardMetabase","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/dashboard-metabase/{id}":{"get":{"operationId":"CatalogController_getOneDashboardMetabase","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/table-metadata":{"get":{"operationId":"CatalogController_getAllTableMetadata","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/table-metadata/{id}":{"delete":{"operationId":"CatalogController_deleteOneTableMetadata","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/column-metadata":{"get":{"operationId":"CatalogController_getOneColumnMetadata","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/column-metadata/{id}":{"delete":{"operationId":"CatalogController_deleteOneColumnMetadata","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data-preview":{"get":{"operationId":"CatalogController_getOneDataPreview","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data-status":{"get":{"operationId":"CatalogController_createDataStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data-description":{"get":{"operationId":"CatalogController_getDataDescription","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]},"post":{"operationId":"CatalogController_createDataDescription","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data-docs":{"get":{"operationId":"CatalogController_getDataDocs","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]},"post":{"operationId":"CatalogController_createDataDocs","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data-rating":{"get":{"operationId":"CatalogController_getDataRating","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]},"post":{"operationId":"CatalogController_createDataRating","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/summary-rating/{id}":{"get":{"operationId":"CatalogController_getSummaryRating","parameters":[],"responses":{"200":{"description":""}},"tags":["Catalog"]}},"/catalog/data-comment":{"get":{"operationId":"CatalogController_getDataComment","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]},"post":{"operationId":"CatalogController_createDataComment","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/data-review":{"get":{"operationId":"CatalogController_getDataReview","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/tags":{"post":{"operationId":"CatalogController_createTags","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]},"get":{"operationId":"CatalogController_findAllTags","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/tags/{id}":{"delete":{"operationId":"CatalogController_deleteTags","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/table-tags":{"post":{"operationId":"CatalogController_createTableTags","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]},"get":{"operationId":"CatalogController_getAllTableTags","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/table-tags/{id}":{"delete":{"operationId":"CatalogController_deleteTableTags","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/table-rules":{"get":{"operationId":"CatalogController_getTableRules","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/catalog/table-rules/{id}":{"delete":{"operationId":"CatalogController_deleteTableRules","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Catalog"]}},"/oauth/hubspot":{"get":{"operationId":"OauthController_oauthHubspot","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"boolean"}}}}},"tags":["oauth"]}},"/oauth/hubspot/callback":{"get":{"operationId":"OauthController_oauthHubspotCallback","parameters":[],"responses":{"200":{"description":""}},"tags":["oauth"]}}},"info":{"title":"Maestro Grpc Documentation","description":"Documentation for Maestro gateway","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"GetAvailableEntitiesRes":{"type":"object","properties":{"entities":{"type":"array","items":{"type":"string"}}},"required":["entities"]},"CredentialsJdbc":{"type":"object","properties":{"jdbc_user":{"type":"string"},"jdbc_password":{"type":"string"},"database":{"type":"string"},"endpoint":{"type":"string"},"port":{"type":"string"},"engine":{"type":"string"},"schema":{"type":"string"}}},"TestConnectionReq":{"type":"object","properties":{"plugin":{"type":"string"},"credentials":{"$ref":"#/components/schemas/CredentialsJdbc"}},"required":["plugin","credentials"]},"TestConnectionRes":{"type":"object","properties":{"connection_state":{"type":"boolean"},"total_entities":{"type":"number"},"database_tables":{"type":"array","items":{"type":"string"}}},"required":["connection_state","total_entities","database_tables"]},"TestConnectionGetColumnsReq":{"type":"object","properties":{"plugin":{"type":"string"},"tables":{"type":"array","items":{"type":"string"}},"credentials":{"$ref":"#/components/schemas/CredentialsJdbc"},"id":{"type":"string"}},"required":["plugin","tables"]},"TestConnectionGetColumnsRes":{"type":"object","properties":{"tables":{"type":"array","items":{"type":"string"}}},"required":["tables"]},"FileFormatParams":{"type":"object","properties":{"file_format":{"type":"string"},"encoding":{"type":"string"}}},"GoogleAnalyticsClientSecrets":{"type":"object","properties":{"type":{"type":"string"},"project_id":{"type":"string"},"private_key_id":{"type":"string"},"private_key":{"type":"string"},"client_email":{"type":"string"},"client_id":{"type":"string"},"auth_uri":{"type":"string"},"token_uri":{"type":"string"},"auth_provider_x509_cert_url":{"type":"string"},"client_x509_cert_url":{"type":"string"}},"required":["type","project_id","private_key_id","private_key","client_email","client_id","auth_uri","token_uri","auth_provider_x509_cert_url","client_x509_cert_url"]},"Credentials":{"type":"object","properties":{"jdbc_user":{"type":"string"},"jdbc_password":{"type":"string"},"database":{"type":"string"},"endpoint":{"type":"string"},"port":{"type":"string"},"engine":{"type":"string"},"schema":{"type":"string"},"connection_type":{"type":"string"},"client_aws_access_key_id":{"type":"string"},"client_aws_secret_access_key":{"type":"string"},"client_bucket":{"type":"string"},"file_to_extract":{"type":"string"},"file_format_params":{"$ref":"#/components/schemas/FileFormatParams"},"view_id":{"type":"string"},"client_secrets":{"$ref":"#/components/schemas/GoogleAnalyticsClientSecrets"},"start_date":{"type":"string"},"end_date":{"type":"string"},"oauth_code":{"type":"string"}},"required":["connection_type"]},"OauthObject":{"type":"object","properties":{"get_tokens_url":{"type":"string"},"get_tokens_url_params":{"type":"string"},"get_tokens_set_response":{"type":"object"},"content_type":{"type":"string"}}},"InputOptions":{"type":"object","properties":{"oauth":{"$ref":"#/components/schemas/OauthObject"},"skip_select_columns":{"type":"boolean"},"skip_select_entities":{"type":"boolean"},"skip_transformation":{"type":"boolean"}}},"CreateInputReq":{"type":"object","properties":{"id":{"type":"string"},"plugin":{"type":"string"},"category":{"type":"string"},"credentials":{"$ref":"#/components/schemas/Credentials"},"name":{"type":"string"},"options":{"$ref":"#/components/schemas/InputOptions"}},"required":["plugin","category","credentials","name"]},"Input":{"type":"object","properties":{"id":{"type":"string"},"category":{"type":"string"},"plugin":{"type":"string"},"name":{"type":"string"},"cron":{"type":"string"},"credentials":{"$ref":"#/components/schemas/Credentials"},"client_id":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","category","plugin","name","cron","credentials","client_id","created_at","updated_at"]}}}} \ No newline at end of file