mirror of
https://github.com/dadosfera/maestro.git
synced 2026-08-31 19:58:21 +00:00
Return payload.permissions verbatim (numeric seqids) instead of translating them to claim strings. Consumers own the seqid->meaning mapping. Drops permission-claims.ts entirely; UserDTO.permissions is now number[]. Co-Authored-By: WOZCODE <contact@withwoz.com>
499 lines
13 KiB
TypeScript
499 lines
13 KiB
TypeScript
import {
|
|
OnModuleInit,
|
|
Inject,
|
|
Injectable,
|
|
ForbiddenException,
|
|
HttpException,
|
|
HttpStatus,
|
|
} from '@nestjs/common';
|
|
import { ClientGrpc } from '@nestjs/microservices';
|
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
|
import { lastValueFrom } from 'rxjs';
|
|
|
|
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
|
import {
|
|
AuthProtoService as AuthServiceInterface,
|
|
UsersProtoService,
|
|
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
|
import {
|
|
AuthSnowflakeSignInRequest,
|
|
AuthSignInRequest,
|
|
AuthRefreshAccessTokenRequest,
|
|
AuthEnableTotpMfaRequest,
|
|
AuthDisableTotpMfaRequest,
|
|
AuthDismissTotpMfaRequest,
|
|
AuthVerifyTotpMfaRequest,
|
|
AuthChangePasswordRequest,
|
|
AuthResetPasswordRequest,
|
|
AuthVerifyResetPasswordCodeRequest,
|
|
AuthConfirmResetPasswordRequest,
|
|
AuthSignInResponse,
|
|
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
|
import { DucClient } from '../duc/client.config';
|
|
import { Metadata } from '@grpc/grpc-js';
|
|
import { BulkEditResponse, UserDTO } from './dtos/login';
|
|
import jwt, { JwtPayload } from 'jsonwebtoken';
|
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
|
import { Request, Response } from 'express';
|
|
|
|
type AuthSession = {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
userId?: string;
|
|
};
|
|
|
|
@Injectable()
|
|
export class AuthClientService implements OnModuleInit {
|
|
logger: DadosferaLogger;
|
|
|
|
private authService: AuthServiceInterface;
|
|
private userService: UsersProtoService;
|
|
|
|
constructor(
|
|
@Inject(DadosferaLogger)
|
|
dadosferaLogger: DadosferaLogger,
|
|
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
|
|
) {
|
|
this.logger = dadosferaLogger.logger;
|
|
}
|
|
|
|
onModuleInit() {
|
|
this.authService = this.grpcClient.getService<AuthServiceInterface>(
|
|
ProtoServices.AuthProtoService,
|
|
);
|
|
|
|
this.userService = this.grpcClient.getService<UsersProtoService>(
|
|
ProtoServices.UsersProtoService,
|
|
);
|
|
}
|
|
|
|
async getPublicKeys() {
|
|
this.logger.info('GetPublicKeys');
|
|
|
|
return lastValueFrom(this.authService.AuthGetPublicKeys({}));
|
|
}
|
|
|
|
async snowflakeSignIn(input: AuthSnowflakeSignInRequest) {
|
|
this.logger.info('snowflakeSignIn');
|
|
|
|
return lastValueFrom(this.authService.AuthSnowflakeSignIn(input));
|
|
}
|
|
|
|
checkDedicatedProxy({ customer }: AuthSignInResponse) {
|
|
const DEDICATED_PROXY = process.env.DEDICATED_PROXY || '';
|
|
this.logger.info(
|
|
'SignIn - Setting customer ID for dedicated proxy: ' + DEDICATED_PROXY,
|
|
);
|
|
this.logger.info('Customer ID: ' + customer.id);
|
|
|
|
if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== customer.id) {
|
|
throw new ForbiddenException();
|
|
}
|
|
|
|
// Bloquear o customer de acesso o maestro publico
|
|
this.logger.info(
|
|
'Check if customer have network policy: ' + customer.modules,
|
|
);
|
|
const hasNetworkPolicyModule = customer.modules.includes('network-policy');
|
|
if (hasNetworkPolicyModule && DEDICATED_PROXY === '') {
|
|
throw new ForbiddenException();
|
|
}
|
|
}
|
|
|
|
async signIn(
|
|
{ username, password, totp }: AuthSignInRequest,
|
|
metadata: Metadata,
|
|
) {
|
|
this.logger.info('SignIn');
|
|
|
|
let result: AuthSignInResponse;
|
|
|
|
try {
|
|
result = await lastValueFrom(
|
|
this.authService.AuthSignIn({ username, password, totp }, metadata),
|
|
);
|
|
} catch (error) {
|
|
this.logger.error('SignIn - Error during sign-in');
|
|
this.logger.error(error);
|
|
throw error;
|
|
}
|
|
|
|
if (result.customer) {
|
|
this.checkDedicatedProxy(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async refreshAccessToken(
|
|
{ refreshToken, userId }: AuthRefreshAccessTokenRequest,
|
|
metadata: Metadata,
|
|
) {
|
|
this.logger.info('RefreshAccessToken');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthRefreshAccessToken(
|
|
{ refreshToken, userId },
|
|
metadata,
|
|
),
|
|
);
|
|
}
|
|
|
|
async changePassword({
|
|
accessToken,
|
|
oldPassword,
|
|
newPassword,
|
|
totpCode,
|
|
}: AuthChangePasswordRequest) {
|
|
this.logger.info('ChangePassword');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthChangePassword({
|
|
accessToken,
|
|
oldPassword,
|
|
newPassword,
|
|
totpCode,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async resetPassword(
|
|
{ username }: AuthResetPasswordRequest,
|
|
metadata: Metadata,
|
|
) {
|
|
this.logger.info('resetPassword');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthResetPassword({ username }, metadata),
|
|
);
|
|
}
|
|
|
|
async verifyResetPasswordCode({
|
|
username,
|
|
code,
|
|
}: AuthVerifyResetPasswordCodeRequest) {
|
|
this.logger.info('verifyResetPasswordCode');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthVerifyResetPasswordCode({ username, code }),
|
|
);
|
|
}
|
|
|
|
async confirmResetPassword(
|
|
{ username, code, newPassword }: AuthConfirmResetPasswordRequest,
|
|
metadata: Metadata,
|
|
) {
|
|
this.logger.info('confirmResetPassword');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthConfirmResetPassword(
|
|
{
|
|
username,
|
|
code,
|
|
newPassword,
|
|
},
|
|
metadata,
|
|
),
|
|
);
|
|
}
|
|
|
|
async enableTotpMFA({ accessToken, password }: AuthEnableTotpMfaRequest) {
|
|
this.logger.info('enableTotpMFA');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthEnableTotpMfa({ accessToken, password }),
|
|
);
|
|
}
|
|
|
|
async disableTotpMFA({ accessToken, password }: AuthDisableTotpMfaRequest) {
|
|
this.logger.info('disableTotpMFA');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthDisableTotpMfa({ accessToken, password }),
|
|
);
|
|
}
|
|
|
|
async dismissTotpMFA({ accessToken }: AuthDismissTotpMfaRequest) {
|
|
this.logger.info('dismissTotpMFA');
|
|
|
|
return lastValueFrom(this.authService.AuthDismissTotpMfa({ accessToken }));
|
|
}
|
|
|
|
async verifyTotp({ accessToken, totp }: AuthVerifyTotpMfaRequest) {
|
|
this.logger.info('disableTotpMFA');
|
|
|
|
return lastValueFrom(
|
|
this.authService.AuthVerifyTotpMfa({ accessToken, totp }),
|
|
);
|
|
}
|
|
|
|
async getSession(session: string) {
|
|
return lastValueFrom(this.authService.AuthGetSession({ session }));
|
|
}
|
|
|
|
async oauthSignIn(data: { username: string; token: string }) {
|
|
const { username, token } = data;
|
|
return lastValueFrom(
|
|
this.authService.AuthOauthSignIn({ token, username, refreshToken: '' }),
|
|
);
|
|
}
|
|
|
|
async blockUsers(
|
|
users: string[],
|
|
metadata: Metadata,
|
|
): Promise<BulkEditResponse> {
|
|
this.logger.info('blockUsers - Service starting');
|
|
|
|
try {
|
|
this.logger.debug('Calling BlockUser gRPC method', {
|
|
metadata: {
|
|
access_token: metadata.get('access_token'),
|
|
language: metadata.get('language'),
|
|
},
|
|
});
|
|
|
|
const response = await lastValueFrom<BulkEditResponse>(
|
|
this.authService.BlockUser({ users }, metadata),
|
|
);
|
|
|
|
this.logger.info('blockUsers - Service success', { response });
|
|
return response;
|
|
} catch (error) {
|
|
this.logger.error('blockUsers - Service error', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async unblockUsers(
|
|
users: string[],
|
|
metadata: Metadata,
|
|
): Promise<BulkEditResponse> {
|
|
this.logger.info('unblockUsers');
|
|
return await lastValueFrom(
|
|
this.authService.UnblockUser({ users }, metadata),
|
|
);
|
|
}
|
|
|
|
async resetUsers(
|
|
users: string[],
|
|
metadata: Metadata,
|
|
): Promise<BulkEditResponse> {
|
|
this.logger.info('resetUsers');
|
|
|
|
try {
|
|
this.logger.debug('Calling ResetUser gRPC method', {
|
|
metadata: {
|
|
access_token: metadata.get('access_token'),
|
|
language: metadata.get('language'),
|
|
},
|
|
});
|
|
|
|
const response = await lastValueFrom<BulkEditResponse>(
|
|
this.authService.ResetUser({ users }, metadata),
|
|
);
|
|
|
|
this.logger.info('resetUsers - Success', { response });
|
|
return response;
|
|
} catch (error) {
|
|
this.logger.error('resetUsers - Error', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async validateUserSession(accessToken: any, resourceHost: string) {
|
|
const payload = await this.validateJwtToken(accessToken);
|
|
|
|
const userDto = await this.getUserfromPayload(payload);
|
|
|
|
this.validateResourceAccess(resourceHost, userDto);
|
|
return userDto;
|
|
}
|
|
|
|
public async refreshUserSession(
|
|
refreshToken: string,
|
|
userId: string,
|
|
originHeader: string,
|
|
): Promise<{
|
|
user: UserDTO;
|
|
authSession: AuthSession;
|
|
}> {
|
|
const metadata = PackTheMetadata({});
|
|
|
|
this.logger.info('Call Refresh Token');
|
|
const refreshCredentials = await this.refreshAccessToken(
|
|
{ refreshToken, userId },
|
|
metadata,
|
|
);
|
|
this.logger.info('Finish Refresh Token');
|
|
|
|
const userDto = await this.validateUserSession(
|
|
refreshCredentials.accessToken,
|
|
originHeader,
|
|
);
|
|
return {
|
|
user: userDto,
|
|
authSession: {
|
|
accessToken: refreshCredentials.accessToken,
|
|
refreshToken: refreshCredentials.refreshToken,
|
|
userId,
|
|
},
|
|
};
|
|
}
|
|
|
|
public writeAuthSession(res: Response, data: AuthSession) {
|
|
let exp = 1000 * 60 * 5; // 5 minutes
|
|
|
|
if (data.accessToken) {
|
|
const { exp: expiration } = jwt.decode(data.accessToken) as JwtPayload;
|
|
exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms
|
|
|
|
this.logger.info('Set Cookie ddf-auth');
|
|
res.cookie('ddf-auth', data.accessToken, {
|
|
domain: '.dadosfera.ai',
|
|
maxAge: exp,
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
|
});
|
|
}
|
|
|
|
if (data.refreshToken) {
|
|
this.logger.info('Set Cookie ddf-refresh-auth');
|
|
res.cookie('ddf-refresh-auth', data.refreshToken, {
|
|
domain: '.dadosfera.ai',
|
|
maxAge: exp,
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
|
});
|
|
}
|
|
|
|
if (data.userId) {
|
|
this.logger.info('Set Cookie ddf-refresh-auth');
|
|
res.cookie('ddf-user-id', data.userId, {
|
|
domain: '.dadosfera.ai',
|
|
maxAge: exp,
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
|
});
|
|
}
|
|
}
|
|
|
|
public cleanUpAuthSession(res: Response) {
|
|
const exp = 1000 * 60 * 3;
|
|
|
|
res.cookie('ddf-auth', '', {
|
|
domain: 'dadosfera.ai',
|
|
maxAge: Date.now() - exp,
|
|
expires: new Date(),
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
|
});
|
|
|
|
res.cookie('ddf-refresh-auth', '', {
|
|
domain: 'dadosfera.ai',
|
|
maxAge: Date.now() - exp,
|
|
expires: new Date(),
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
|
});
|
|
|
|
this.logger.info('Clean cookie sessions');
|
|
}
|
|
|
|
private async validateJwtToken(token: string) {
|
|
const decoded: any = token && jwt.decode(token, { complete: true });
|
|
if (!decoded) throw new Error('Invalid token');
|
|
|
|
const { kid } = decoded.header;
|
|
// Busca a chave pública
|
|
const { keys } = await this.getPublicKeys();
|
|
const pemValue = keys.find((k) => k.kid === kid)?.pem;
|
|
if (!pemValue) throw new Error('Public key not found');
|
|
jwt.verify(token, pemValue);
|
|
|
|
return decoded.payload;
|
|
}
|
|
|
|
private async getUserfromPayload(payload: JwtPayload): Promise<UserDTO> {
|
|
this.logger.info('getUser');
|
|
|
|
const metadata = PackTheMetadata({
|
|
customer_id: payload.customer_id,
|
|
});
|
|
|
|
const { user } = await lastValueFrom(
|
|
this.userService.UserFindOneById({ id: payload.user_id }, metadata),
|
|
);
|
|
|
|
const userDto: UserDTO = {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
jobTitle: user?.jobTitle || null,
|
|
department: user?.department || null,
|
|
hierarchy: user?.hierarchy || null,
|
|
customer: {
|
|
id: payload.customer_id,
|
|
name: payload.customer_name,
|
|
tier: payload.customer_tier,
|
|
},
|
|
// Raw permission seqids from the JWT. Consumers own the seqid->meaning
|
|
// mapping (e.g. Orchest's auth-server); Maestro reports them as-is.
|
|
permissions: payload.permissions ?? [],
|
|
};
|
|
|
|
return userDto;
|
|
}
|
|
|
|
private validateResourceAccess(host: string, user: UserDTO) {
|
|
this.logger.info(
|
|
"Validate whether the source URL is a resource belonging to the user's client",
|
|
);
|
|
this.logger.info('Host: ' + host);
|
|
this.logger.info('Customer: ' + user.customer.name);
|
|
|
|
const hostParts = host.split('.');
|
|
const domain = hostParts[0];
|
|
const isResouceStg = hostParts[1] === 'stg';
|
|
|
|
const notFoundCustomerInDomain = !domain.includes('-')
|
|
|
|
if (notFoundCustomerInDomain) {
|
|
this.logger.info(`Not found Customer Name in domain`);
|
|
return;
|
|
}
|
|
|
|
const domainParts = domain.split('-');
|
|
|
|
const customerInDomain = domainParts[domainParts.length - 1];
|
|
|
|
if (isResouceStg && process.env.ENV !== 'stg') {
|
|
this.logger.error(`Customer ${user.customer.name} cannot access ${host}`);
|
|
throw new HttpException(
|
|
`Customer ${user.customer.name} cannot access ${host}`,
|
|
HttpStatus.FORBIDDEN
|
|
);
|
|
}
|
|
|
|
if (customerInDomain != user.customer.name) {
|
|
this.logger.error(`Customer ${user.customer.name} cannot access ${host}`);
|
|
throw new HttpException(
|
|
`Customer ${user.customer.name} cannot access ${host}`,
|
|
HttpStatus.FORBIDDEN
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|