This commit is contained in:
Guillim 2024-12-04 15:03:06 +01:00 committed by GitHub
parent c735026f6c
commit 2c0d3e93d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 162 additions and 129 deletions

View File

@ -0,0 +1,51 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Response } from 'express';
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
import { ExceptionHandlerWorkspace } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-workspace.interface';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
export const handleException = (
exception: AuthException,
exceptionHandlerService: ExceptionHandlerService,
user?: ExceptionHandlerUser,
workspace?: ExceptionHandlerWorkspace,
): void => {
exceptionHandlerService.captureExceptions([exception], { user, workspace });
};
interface RequestAndParams {
request: Request | null;
params: any;
}
@Injectable({ scope: Scope.REQUEST })
export class AuthExceptionHandlerService {
constructor(
private readonly exceptionHandlerService: ExceptionHandlerService,
@Inject(REQUEST)
private readonly request: RequestAndParams | null,
) {}
handleError = (
exception: AuthException,
response: Response<any, Record<string, any>>,
errorCode?: number,
user?: ExceptionHandlerUser,
workspace?: ExceptionHandlerWorkspace,
): Response<any, Record<string, any>> | undefined => {
const params = this.request?.params;
if (params?.workspaceId)
workspace = { ...workspace, id: params.workspaceId };
if (params?.userId) user = { ...user, id: params.userId };
handleException(exception, this.exceptionHandlerService, user, workspace);
return response.status(errorCode || 500).send(exception.message);
};
}

View File

@ -19,4 +19,8 @@ export enum AuthExceptionCode {
OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED',
SSO_AUTH_FAILED = 'SSO_AUTH_FAILED',
USE_SSO_AUTH = 'USE_SSO_AUTH',
SIGNUP_DISABLED = 'SIGNUP_DISABLED',
GOOGLE_API_AUTH_DISABLED = 'GOOGLE_API_AUTH_DISABLED',
MICROSOFT_API_AUTH_DISABLED = 'MICROSOFT_API_AUTH_DISABLED',
MISSING_ENVIRONMENT_VARIABLE = 'MISSING_ENVIRONMENT_VARIABLE',
}

View File

@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { AuthExceptionHandlerService } from 'src/engine/core-modules/auth/auth-exception-handler.service';
import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller';
import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller';
@ -23,6 +24,8 @@ import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
@ -34,15 +37,13 @@ import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/worksp
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { AuthResolver } from './auth.resolver';
@ -102,6 +103,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
ResetPasswordService,
SwitchWorkspaceService,
TransientTokenService,
AuthExceptionHandlerService,
ApiKeyService,
OAuthService,
],

View File

@ -1,13 +1,8 @@
import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { AuthExceptionHandlerService } from 'src/engine/core-modules/auth/auth-exception-handler.service';
import {
AuthException,
AuthExceptionCode,
@ -15,19 +10,51 @@ import {
@Catch(AuthException)
export class AuthRestApiExceptionFilter implements ExceptionFilter {
catch(exception: AuthException, _: ArgumentsHost) {
constructor(
private readonly authExceptionHandlerService: AuthExceptionHandlerService,
) {}
catch(exception: AuthException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
switch (exception.code) {
case AuthExceptionCode.USER_NOT_FOUND:
case AuthExceptionCode.CLIENT_NOT_FOUND:
throw new NotFoundException(exception.message);
return this.authExceptionHandlerService.handleError(
exception,
response,
404,
);
case AuthExceptionCode.INVALID_INPUT:
throw new BadRequestException(exception.message);
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
throw new UnauthorizedException(exception.message);
case AuthExceptionCode.INVALID_DATA:
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
return this.authExceptionHandlerService.handleError(
exception,
response,
400,
);
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
return this.authExceptionHandlerService.handleError(
exception,
response,
401,
);
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
case AuthExceptionCode.SIGNUP_DISABLED:
return this.authExceptionHandlerService.handleError(
exception,
response,
403,
);
case AuthExceptionCode.INTERNAL_SERVER_ERROR:
default:
throw new InternalServerErrorException(exception.message);
return this.authExceptionHandlerService.handleError(
exception,
response,
500,
);
}
}
}

View File

@ -1,13 +1,13 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@ -41,9 +41,9 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
) {
throw new EnvironmentException(
throw new AuthException(
'Google apis auth is not enabled',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
);
}

View File

@ -1,13 +1,13 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@ -27,7 +27,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const { workspaceId } =
const { workspaceId, userId } =
await this.transientTokenService.verifyTransientToken(
request.query.transientToken,
);
@ -37,13 +37,23 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
workspaceId,
);
setRequestExtraParams(request, {
transientToken: request.query.transientToken,
redirectLocation: request.query.redirectLocation,
calendarVisibility: request.query.calendarVisibility,
messageVisibility: request.query.messageVisibility,
loginHint: request.query.loginHint,
userId: userId,
workspaceId: workspaceId,
});
if (
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
) {
throw new EnvironmentException(
throw new AuthException(
'Google apis auth is not enabled',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
);
}
@ -52,13 +62,6 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
{},
isGmailSendEmailScopeEnabled,
);
setRequestExtraParams(request, {
transientToken: request.query.transientToken,
redirectLocation: request.query.redirectLocation,
calendarVisibility: request.query.calendarVisibility,
messageVisibility: request.query.messageVisibility,
loginHint: request.query.loginHint,
});
const activate = (await super.canActivate(context)) as boolean;

View File

@ -2,11 +2,11 @@ import { CanActivate, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
@ -15,9 +15,9 @@ export class GoogleProviderEnabledGuard implements CanActivate {
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) {
throw new EnvironmentException(
throw new AuthException(
'Google auth is not enabled',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
AuthExceptionCode.GOOGLE_API_AUTH_DISABLED,
);
}

View File

@ -2,11 +2,11 @@ import { CanActivate, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
@ -15,9 +15,9 @@ export class MicrosoftProviderEnabledGuard implements CanActivate {
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) {
throw new EnvironmentException(
throw new AuthException(
'Microsoft auth is not enabled',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED,
);
}

View File

@ -5,9 +5,9 @@ import { CanActivate, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@Injectable()
@ -16,9 +16,9 @@ export class SSOProviderEnabledGuard implements CanActivate {
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('ENTERPRISE_KEY')) {
throw new EnvironmentException(
throw new AuthException(
'Enterprise key must be defined to use SSO',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE,
);
}

View File

@ -18,24 +18,20 @@ import {
hashPassword,
PASSWORD_REGEX,
} from 'src/engine/core-modules/auth/auth.util';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import {
Workspace,
WorkspaceActivationStatus,
} from 'src/engine/core-modules/workspace/workspace.entity';
import { getImageBufferFromUrl } from 'src/utils/image';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
import { getImageBufferFromUrl } from 'src/utils/image';
export type SignInUpServiceInput = {
email: string;
@ -299,9 +295,9 @@ export class SignInUpService {
// let the creation of the first workspace
if (workspacesCount > 0) {
throw new EnvironmentException(
throw new AuthException(
'New workspace setup is disabled',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
AuthExceptionCode.SIGNUP_DISABLED,
);
}
}

View File

@ -17,10 +17,6 @@ import {
AuthContext,
JwtPayload,
} from 'src/engine/core-modules/auth/types/auth-context.type';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { User } from 'src/engine/core-modules/user/user.entity';
@ -45,13 +41,6 @@ export class AccessTokenService {
): Promise<AuthToken> {
const expiresIn = this.environmentService.get('ACCESS_TOKEN_EXPIRES_IN');
if (!expiresIn) {
throw new EnvironmentException(
'Expiration time for access token is not set',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
);
}
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const user = await this.userRepository.findOne({

View File

@ -1,6 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EnvironmentException } from 'src/engine/core-modules/environment/environment.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
@ -70,14 +69,6 @@ describe('LoginTokenService', () => {
{ secret: mockSecret, expiresIn: mockExpiresIn },
);
});
it('should throw an error if LOGIN_TOKEN_EXPIRES_IN is not set', async () => {
jest.spyOn(environmentService, 'get').mockReturnValue(undefined);
await expect(
service.generateLoginToken('test@example.com'),
).rejects.toThrow(EnvironmentException);
});
});
describe('verifyLoginToken', () => {

View File

@ -4,10 +4,6 @@ import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
@ -23,13 +19,6 @@ export class LoginTokenService {
const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN');
if (!expiresIn) {
throw new EnvironmentException(
'Expiration time for access token is not set',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
);
}
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const jwtPayload = {
sub: email,

View File

@ -1,6 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EnvironmentException } from 'src/engine/core-modules/environment/environment.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
@ -82,14 +81,6 @@ describe('TransientTokenService', () => {
}),
);
});
it('should throw an error if SHORT_TERM_TOKEN_EXPIRES_IN is not set', async () => {
jest.spyOn(environmentService, 'get').mockReturnValue(undefined);
await expect(
service.generateTransientToken('member-id', 'user-id', 'workspace-id'),
).rejects.toThrow(EnvironmentException);
});
});
describe('verifyTransientToken', () => {

View File

@ -4,10 +4,6 @@ import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
import {
EnvironmentException,
EnvironmentExceptionCode,
} from 'src/engine/core-modules/environment/environment.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
@ -31,13 +27,6 @@ export class TransientTokenService {
'SHORT_TERM_TOKEN_EXPIRES_IN',
);
if (!expiresIn) {
throw new EnvironmentException(
'Expiration time for access token is not set',
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
);
}
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const jwtPayload = {
sub: workspaceMemberId,

View File

@ -10,6 +10,8 @@ type GoogleAPIsRequestExtraParams = {
calendarVisibility?: string;
messageVisibility?: string;
loginHint?: string;
userId?: string;
workspaceId?: string;
};
export const setRequestExtraParams = (
@ -22,6 +24,8 @@ export const setRequestExtraParams = (
calendarVisibility,
messageVisibility,
loginHint,
userId,
workspaceId,
} = params;
if (!transientToken) {
@ -44,7 +48,16 @@ export const setRequestExtraParams = (
if (messageVisibility) {
request.params.messageVisibility = messageVisibility;
}
if (loginHint) {
request.params.loginHint = loginHint;
}
if (userId) {
request.params.userId = userId;
}
if (workspaceId) {
request.params.workspaceId = workspaceId;
}
};

View File

@ -1,12 +0,0 @@
import { CustomException } from 'src/utils/custom-exception';
export class EnvironmentException extends CustomException {
code: EnvironmentExceptionCode;
constructor(message: string, code: EnvironmentExceptionCode) {
super(message, code);
}
}
export enum EnvironmentExceptionCode {
ENVIRONMENT_VARIABLES_NOT_FOUND = 'ENVIRONMENT_VARIABLES_NOT_FOUND',
}