We must separate the concept of hydratation which happens at the request level (take the token and pass auth/user context), from the concept of authorization which happens at the query/endpoint/mutation level. Previously, hydratation exemption happened at the operation name level which is not correct because the operation name is meaningless and optional. Still this gave an impression of security by enforcing a blacklist. So in this PR we introduce linting rule that aim to achieve a similar behavior, now every api method has to have a guard. That way if and endpoint is not protected by AuthUserGuard or AuthWorspaceGuard, then it has to be stated explicitly next to its code. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
171 lines
5.6 KiB
TypeScript
171 lines
5.6 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
import { Request, Response } from 'express';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
|
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
|
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
|
import { getAuthExceptionRestStatus } from 'src/engine/core-modules/auth/utils/get-auth-exception-rest-status.util';
|
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
|
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
|
import { INTERNAL_SERVER_ERROR } from 'src/engine/middlewares/constants/default-error-message.constant';
|
|
import {
|
|
handleException,
|
|
handleExceptionAndConvertToGraphQLError,
|
|
} from 'src/engine/utils/global-exception-handler.util';
|
|
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
|
import { CustomException } from 'src/utils/custom-exception';
|
|
|
|
@Injectable()
|
|
export class MiddlewareService {
|
|
constructor(
|
|
private readonly accessTokenService: AccessTokenService,
|
|
private readonly workspaceStorageCacheService: WorkspaceCacheStorageService,
|
|
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
|
private readonly dataSourceService: DataSourceService,
|
|
private readonly exceptionHandlerService: ExceptionHandlerService,
|
|
private readonly jwtWrapperService: JwtWrapperService,
|
|
) {}
|
|
|
|
public isTokenPresent(request: Request): boolean {
|
|
const token = this.jwtWrapperService.extractJwtFromRequest()(request);
|
|
|
|
return !!token;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
public writeRestResponseOnExceptionCaught(res: Response, error: any) {
|
|
const statusCode = this.getStatus(error);
|
|
|
|
// capture and handle custom exceptions
|
|
handleException({
|
|
exception: error as CustomException,
|
|
exceptionHandlerService: this.exceptionHandlerService,
|
|
statusCode,
|
|
});
|
|
|
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
res.write(
|
|
JSON.stringify({
|
|
statusCode,
|
|
messages: [error?.message || INTERNAL_SERVER_ERROR],
|
|
error: error?.code || ErrorCode.INTERNAL_SERVER_ERROR,
|
|
}),
|
|
);
|
|
|
|
res.end();
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
public writeGraphqlResponseOnExceptionCaught(res: Response, error: any) {
|
|
let errors;
|
|
|
|
if (error instanceof AuthException) {
|
|
try {
|
|
const authFilter = new AuthGraphqlApiExceptionFilter();
|
|
|
|
authFilter.catch(error);
|
|
} catch (transformedError) {
|
|
errors = [transformedError];
|
|
}
|
|
} else {
|
|
errors = [
|
|
handleExceptionAndConvertToGraphQLError(
|
|
error as Error,
|
|
this.exceptionHandlerService,
|
|
),
|
|
];
|
|
}
|
|
|
|
const statusCode = 200;
|
|
|
|
res.writeHead(statusCode, {
|
|
'Content-Type': 'application/json',
|
|
});
|
|
|
|
res.write(
|
|
JSON.stringify({
|
|
errors,
|
|
}),
|
|
);
|
|
|
|
res.end();
|
|
}
|
|
|
|
public async hydrateRestRequest(request: Request) {
|
|
const data = await this.accessTokenService.validateTokenByRequest(request);
|
|
const metadataVersion =
|
|
await this.workspaceStorageCacheService.getMetadataVersion(
|
|
data.workspace.id,
|
|
);
|
|
|
|
if (metadataVersion === undefined) {
|
|
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
|
workspaceId: data.workspace.id,
|
|
});
|
|
throw new Error('Metadata cache version not found');
|
|
}
|
|
|
|
const dataSourcesMetadata =
|
|
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
|
data.workspace.id,
|
|
);
|
|
|
|
if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) {
|
|
throw new Error('No data sources found');
|
|
}
|
|
|
|
this.bindDataToRequestObject(data, request, metadataVersion);
|
|
}
|
|
|
|
public async hydrateGraphqlRequest(request: Request) {
|
|
if (!this.isTokenPresent(request)) {
|
|
return;
|
|
}
|
|
|
|
const data = await this.accessTokenService.validateTokenByRequest(request);
|
|
const metadataVersion =
|
|
await this.workspaceStorageCacheService.getMetadataVersion(
|
|
data.workspace.id,
|
|
);
|
|
|
|
this.bindDataToRequestObject(data, request, metadataVersion);
|
|
}
|
|
|
|
private hasErrorStatus(error: unknown): error is { status: number } {
|
|
return isDefined((error as { status: number })?.status);
|
|
}
|
|
|
|
private bindDataToRequestObject(
|
|
data: AuthContext,
|
|
request: Request,
|
|
metadataVersion: number | undefined,
|
|
) {
|
|
request.user = data.user;
|
|
request.apiKey = data.apiKey;
|
|
request.workspace = data.workspace;
|
|
request.workspaceId = data.workspace.id;
|
|
request.workspaceMetadataVersion = metadataVersion;
|
|
request.workspaceMemberId = data.workspaceMemberId;
|
|
request.userWorkspaceId = data.userWorkspaceId;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
private getStatus(error: any): number {
|
|
if (this.hasErrorStatus(error)) {
|
|
return error.status;
|
|
}
|
|
|
|
if (error instanceof AuthException) {
|
|
return getAuthExceptionRestStatus(error);
|
|
}
|
|
|
|
return 500;
|
|
}
|
|
}
|