Centralize and standardize impersonation validation rules (#21717)
# Introduction Followup https://github.com/twentyhq/twenty/pull/21707 ## Behavioral change worth calling out Server-level impersonation now requires verified 2FA outside development at every checkpoint (generation, exchange, and per-request). In main the 2FA gate only existed in ImpersonationService. This is the right tightening, but it means existing server-admin impersonation sessions in production for admins without verified 2FA will now be rejected on the next request, not just at token creation. cc @s0yd4RK <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21717?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: s0yd4RK <285671363+s0yd4RK@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { ConnectionProviderModule } from 'src/engine/core-modules/application/connection-provider/connection-provider.module';
|
||||
import { ConnectionProviderOAuthController } from 'src/engine/core-modules/application/connection-provider/connection-provider-oauth.controller';
|
||||
import { ConnectionProviderModule } from 'src/engine/core-modules/application/connection-provider/connection-provider.module';
|
||||
import { ApplicationConnectionsModule } from 'src/engine/core-modules/application/connection-provider/connections/application-connections.module';
|
||||
import { EventLogEmitterModule } from 'src/engine/core-modules/event-logs/emit/event-log-emitter.module';
|
||||
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';
|
||||
@@ -40,10 +40,12 @@ import { SubdomainManagerModule } from 'src/engine/core-modules/domain/subdomain
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { EmailVerificationModule } from 'src/engine/core-modules/email-verification/email-verification.module';
|
||||
import { EnterpriseModule } from 'src/engine/core-modules/enterprise/enterprise.module';
|
||||
import { EventLogEmitterModule } from 'src/engine/core-modules/event-logs/emit/event-log-emitter.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 { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
|
||||
import { ImpersonationAuthorizationModule } from 'src/engine/core-modules/impersonation/impersonation-authorization.module';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
@@ -59,7 +61,6 @@ import { UserEntity } 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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountTokenEncryptionModule } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.module';
|
||||
@@ -68,8 +69,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module';
|
||||
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
|
||||
import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module';
|
||||
|
||||
@@ -109,6 +110,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
WorkspaceInvitationModule,
|
||||
EmailVerificationModule,
|
||||
GuardRedirectModule,
|
||||
ImpersonationAuthorizationModule,
|
||||
MetricsModule,
|
||||
PermissionsModule,
|
||||
TwoFactorAuthenticationModule,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/api-key/services/api-key.service';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { EventLogEmitterService } from 'src/engine/core-modules/event-logs/emit/event-log-emitter.service';
|
||||
import { ImpersonationAuthorizationService } from 'src/engine/core-modules/impersonation/services/impersonation-authorization.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
@@ -120,6 +121,10 @@ describe('AuthResolver', () => {
|
||||
provide: EmailVerificationTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ImpersonationAuthorizationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {},
|
||||
|
||||
@@ -60,6 +60,9 @@ import { EmailVerificationService } from 'src/engine/core-modules/email-verifica
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||
import { IMPERSONATION_DENIAL_BY_REASON } from 'src/engine/core-modules/impersonation/constants/impersonation-denial-by-reason.constant';
|
||||
import { IMPERSONATION_DENIAL_LOG_MESSAGE_BY_REASON } from 'src/engine/core-modules/impersonation/constants/impersonation-denial-log-message-by-reason.constant';
|
||||
import { ImpersonationAuthorizationService } from 'src/engine/core-modules/impersonation/services/impersonation-authorization.service';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import { TwoFactorAuthenticationVerificationInput } from 'src/engine/core-modules/two-factor-authentication/dto/two-factor-authentication-verification.input';
|
||||
import { TwoFactorAuthenticationExceptionFilter } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication-exception.filter';
|
||||
@@ -80,7 +83,6 @@ import { RequireAccessTokenGuard } from 'src/engine/guards/require-access-token.
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
|
||||
import { ApiKeyToken } from './dto/api-key-token.dto';
|
||||
@@ -132,7 +134,7 @@ export class AuthResolver {
|
||||
private emailVerificationTokenService: EmailVerificationTokenService,
|
||||
private ssoService: SSOService,
|
||||
private readonly eventLogEmitterService: EventLogEmitterService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly impersonationAuthorizationService: ImpersonationAuthorizationService,
|
||||
private readonly subdomainManagerService: SubdomainManagerService,
|
||||
) {}
|
||||
|
||||
@@ -702,7 +704,7 @@ export class AuthResolver {
|
||||
const impersonatorUserWorkspace =
|
||||
await this.userWorkspaceRepository.findOne({
|
||||
where: { id: impersonatorUserWorkspaceId },
|
||||
relations: ['user', 'workspace'],
|
||||
relations: ['user', 'workspace', 'twoFactorAuthenticationMethods'],
|
||||
});
|
||||
|
||||
const toImpersonateUserWorkspace =
|
||||
@@ -733,75 +735,49 @@ export class AuthResolver {
|
||||
);
|
||||
}
|
||||
|
||||
const isServerLevelImpersonation =
|
||||
toImpersonateUserWorkspace.workspace.id !==
|
||||
impersonatorUserWorkspace.workspace.id;
|
||||
|
||||
const eventLogContext = this.eventLogEmitterService.createContext({
|
||||
workspaceId: workspace.id,
|
||||
userId: impersonatorUserWorkspace.user.id,
|
||||
});
|
||||
|
||||
const impersonationLevel =
|
||||
this.impersonationAuthorizationService.getImpersonationLevel(
|
||||
impersonatorUserWorkspace,
|
||||
toImpersonateUserWorkspace,
|
||||
);
|
||||
|
||||
void eventLogContext.insertWorkspaceEvent(IMPERSONATION_EVENT, {
|
||||
level: isServerLevelImpersonation ? 'server' : 'workspace',
|
||||
level: impersonationLevel,
|
||||
action: 'token_exchange_attempt',
|
||||
message: `Impersonation token exchange attempt for ${targetUserEmail} by ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
|
||||
const hasServerLevelImpersonatePermission =
|
||||
impersonatorUserWorkspace.user.canImpersonate === true &&
|
||||
toImpersonateUserWorkspace.workspace.allowImpersonation === true;
|
||||
|
||||
if (isServerLevelImpersonation) {
|
||||
if (!hasServerLevelImpersonatePermission) {
|
||||
void eventLogContext.insertWorkspaceEvent(IMPERSONATION_EVENT, {
|
||||
level: 'server',
|
||||
action: 'token_exchange_failed',
|
||||
message: `Server level impersonation not allowed for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
|
||||
throw new AuthException(
|
||||
'Server level impersonation not allowed on this workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
void eventLogContext.insertWorkspaceEvent(IMPERSONATION_EVENT, {
|
||||
level: 'server',
|
||||
action: 'token_exchange_success',
|
||||
message: `Impersonation token exchanged for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
|
||||
return {
|
||||
workspaceId: workspace.id,
|
||||
impersonatorUserWorkspaceId: impersonatorUserWorkspace.id,
|
||||
impersonatedUserWorkspaceId: toImpersonateUserWorkspace.id,
|
||||
impersonatorUserId: impersonatorUserWorkspace.user.id,
|
||||
impersonatedUserId: toImpersonateUserWorkspace.user.id,
|
||||
};
|
||||
}
|
||||
|
||||
const hasWorkspaceLevelImpersonatePermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId: impersonatorUserWorkspace.id,
|
||||
setting: PermissionFlagType.IMPERSONATE,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!hasWorkspaceLevelImpersonatePermission) {
|
||||
void eventLogContext.insertWorkspaceEvent(IMPERSONATION_EVENT, {
|
||||
level: 'workspace',
|
||||
action: 'token_exchange_failed',
|
||||
message: `Impersonation not allowed for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
throw new AuthException(
|
||||
'Impersonation not allowed',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
const authorizationResult =
|
||||
await this.impersonationAuthorizationService.checkImpersonationAuthorization(
|
||||
impersonatorUserWorkspace,
|
||||
toImpersonateUserWorkspace,
|
||||
);
|
||||
|
||||
if (!authorizationResult.allowed) {
|
||||
void eventLogContext.insertWorkspaceEvent(IMPERSONATION_EVENT, {
|
||||
level: authorizationResult.level,
|
||||
action: 'token_exchange_failed',
|
||||
message: IMPERSONATION_DENIAL_LOG_MESSAGE_BY_REASON[
|
||||
authorizationResult.reason
|
||||
]({
|
||||
targetUserEmail,
|
||||
impersonatorUserId: impersonatorUserWorkspace.user.id,
|
||||
}),
|
||||
});
|
||||
|
||||
const { message, exceptionCode, userFriendlyMessage } =
|
||||
IMPERSONATION_DENIAL_BY_REASON[authorizationResult.reason];
|
||||
|
||||
throw new AuthException(message, exceptionCode, { userFriendlyMessage });
|
||||
}
|
||||
|
||||
void eventLogContext.insertWorkspaceEvent(IMPERSONATION_EVENT, {
|
||||
level: 'workspace',
|
||||
level: authorizationResult.level,
|
||||
action: 'token_exchange_success',
|
||||
message: `Impersonation token exchanged for ${targetUserEmail} by userId ${impersonatorUserWorkspace.user.id}`,
|
||||
});
|
||||
|
||||
+13
-1
@@ -8,6 +8,8 @@ import {
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { type JwtPayload } from 'src/engine/core-modules/auth/types/jwt-payload.type';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum';
|
||||
import { ImpersonationAuthorizationService } from 'src/engine/core-modules/impersonation/services/impersonation-authorization.service';
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
import { JwtAuthStrategy } from './jwt.auth.strategy';
|
||||
@@ -17,6 +19,7 @@ describe('JwtAuthStrategy', () => {
|
||||
let userWorkspaceRepository: any;
|
||||
let jwtWrapperService: any;
|
||||
let permissionsService: any;
|
||||
let twentyConfigService: any;
|
||||
let workspaceCacheService: any;
|
||||
let coreEntityCacheService: any;
|
||||
|
||||
@@ -52,6 +55,12 @@ describe('JwtAuthStrategy', () => {
|
||||
userHasWorkspaceSettingPermission: jest.fn(),
|
||||
};
|
||||
|
||||
twentyConfigService = {
|
||||
get: jest.fn((key: string) =>
|
||||
key === 'NODE_ENV' ? NodeEnvironment.DEVELOPMENT : undefined,
|
||||
),
|
||||
};
|
||||
|
||||
workspaceCacheService = {
|
||||
getOrRecompute: jest.fn(
|
||||
async (workspaceId: string, cacheKeys: string[]) => {
|
||||
@@ -118,9 +127,12 @@ describe('JwtAuthStrategy', () => {
|
||||
new JwtAuthStrategy(
|
||||
jwtWrapperService,
|
||||
userWorkspaceRepository,
|
||||
permissionsService,
|
||||
workspaceCacheService,
|
||||
coreEntityCacheService,
|
||||
new ImpersonationAuthorizationService(
|
||||
permissionsService,
|
||||
twentyConfigService,
|
||||
),
|
||||
);
|
||||
|
||||
describe('API_KEY validation', () => {
|
||||
|
||||
+20
-57
@@ -4,32 +4,32 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type SecretOrKeyProvider, Strategy } from 'passport-jwt';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { type AccessTokenJwtPayload } from 'src/engine/core-modules/auth/types/access-token-jwt-payload.type';
|
||||
import { type ApiKeyTokenJwtPayload } from 'src/engine/core-modules/auth/types/api-key-token-jwt-payload.type';
|
||||
import { ApplicationAccessTokenJwtPayload } from 'src/engine/core-modules/auth/types/application-access-token-jwt-payload.type';
|
||||
import {
|
||||
type AuthContext,
|
||||
type AuthContextUser,
|
||||
} from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { type AccessTokenJwtPayload } from 'src/engine/core-modules/auth/types/access-token-jwt-payload.type';
|
||||
import { type ApiKeyTokenJwtPayload } from 'src/engine/core-modules/auth/types/api-key-token-jwt-payload.type';
|
||||
import { ApplicationAccessTokenJwtPayload } from 'src/engine/core-modules/auth/types/application-access-token-jwt-payload.type';
|
||||
import { type JwtPayload } from 'src/engine/core-modules/auth/types/jwt-payload.type';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum';
|
||||
import { type PlaygroundTokenJwtPayload } from 'src/engine/core-modules/auth/types/playground-token-jwt-payload.type';
|
||||
import { type WorkspaceAgnosticTokenJwtPayload } from 'src/engine/core-modules/auth/types/workspace-agnostic-token-jwt-payload.type';
|
||||
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { IMPERSONATION_DENIAL_BY_REASON } from 'src/engine/core-modules/impersonation/constants/impersonation-denial-by-reason.constant';
|
||||
import { ImpersonationAuthorizationService } from 'src/engine/core-modules/impersonation/services/impersonation-authorization.service';
|
||||
import { JWT_SUPPORTED_VERIFY_ALGORITHMS } from 'src/engine/core-modules/jwt/constants/jwt-algorithm.constant';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { type FlatUserWorkspace } from 'src/engine/core-modules/user-workspace/types/flat-user-workspace.type';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
@@ -37,9 +37,9 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
private readonly impersonationAuthorizationService: ImpersonationAuthorizationService,
|
||||
) {
|
||||
const secretOrKeyProvider: SecretOrKeyProvider = (
|
||||
_request,
|
||||
@@ -272,7 +272,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
const impersonatorUserWorkspace =
|
||||
await this.userWorkspaceRepository.findOne({
|
||||
where: { id: payload.impersonatorUserWorkspaceId },
|
||||
relations: ['user', 'workspace'],
|
||||
relations: ['user', 'workspace', 'twoFactorAuthenticationMethods'],
|
||||
});
|
||||
|
||||
const impersonatedUserWorkspace =
|
||||
@@ -291,54 +291,17 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
);
|
||||
}
|
||||
|
||||
const isServerLevelImpersonation =
|
||||
impersonatorUserWorkspace.workspace.id !==
|
||||
impersonatedUserWorkspace.workspace.id;
|
||||
|
||||
const hasServerLevelImpersonatePermission =
|
||||
impersonatorUserWorkspace.user.canImpersonate === true &&
|
||||
impersonatedUserWorkspace.workspace.allowImpersonation === true;
|
||||
|
||||
if (isServerLevelImpersonation) {
|
||||
if (!hasServerLevelImpersonatePermission)
|
||||
throw new AuthException(
|
||||
'Server level impersonation not allowed',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
|
||||
return {
|
||||
impersonatorUserWorkspaceId: payload.impersonatorUserWorkspaceId,
|
||||
impersonatedUserWorkspaceId: payload.impersonatedUserWorkspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
const hasWorkspaceLevelImpersonatePermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId: impersonatorUserWorkspace.id,
|
||||
setting: PermissionFlagType.IMPERSONATE,
|
||||
workspaceId: impersonatedUserWorkspace.workspace.id,
|
||||
});
|
||||
|
||||
if (!hasWorkspaceLevelImpersonatePermission) {
|
||||
throw new AuthException(
|
||||
'Impersonation not allowed',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
const authorizationResult =
|
||||
await this.impersonationAuthorizationService.checkImpersonationAuthorization(
|
||||
impersonatorUserWorkspace,
|
||||
impersonatedUserWorkspace,
|
||||
);
|
||||
}
|
||||
|
||||
const targetHasAdminPrivileges =
|
||||
impersonatedUserWorkspace.user.canImpersonate === true ||
|
||||
impersonatedUserWorkspace.user.canAccessFullAdminPanel === true;
|
||||
if (!authorizationResult.allowed) {
|
||||
const { message, exceptionCode, userFriendlyMessage } =
|
||||
IMPERSONATION_DENIAL_BY_REASON[authorizationResult.reason];
|
||||
|
||||
const impersonatorHasAdminPrivileges =
|
||||
impersonatorUserWorkspace.user.canImpersonate === true ||
|
||||
impersonatorUserWorkspace.user.canAccessFullAdminPanel === true;
|
||||
|
||||
if (targetHasAdminPrivileges && !impersonatorHasAdminPrivileges) {
|
||||
throw new AuthException(
|
||||
'Cannot impersonate a user with admin privileges',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
throw new AuthException(message, exceptionCode, { userFriendlyMessage });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { RenewTokenService } from 'src/engine/core-modules/auth/token/services/renew-token.service';
|
||||
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
|
||||
import { ImpersonationAuthorizationModule } from 'src/engine/core-modules/impersonation/impersonation-authorization.module';
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
@@ -33,6 +34,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
PermissionsModule,
|
||||
WorkspaceCacheModule,
|
||||
CoreEntityCacheModule,
|
||||
ImpersonationAuthorizationModule,
|
||||
],
|
||||
providers: [
|
||||
RenewTokenService,
|
||||
|
||||
+1
@@ -45,6 +45,7 @@ export const authGraphqlApiExceptionHandler = (exception: AuthException) => {
|
||||
case AuthExceptionCode.TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
});
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
case AuthExceptionCode.APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED:
|
||||
|
||||
Reference in New Issue
Block a user