[permissions] Add workspace + security settings permission gates (#10204)
In this PR - closing https://github.com/twentyhq/core-team-issues/issues/313 - adding permission gates on workspace settings and security settings - adding integration tests for each of the protected setting and security
This commit is contained in:
@ -143,6 +143,23 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const permissionsEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (permissionsEnabled) {
|
||||
await this.validateSecurityPermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
await this.validateWorkspacePermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.subdomain && workspace.subdomain !== payload.subdomain) {
|
||||
await this.validateSubdomainUpdate(payload.subdomain);
|
||||
}
|
||||
@ -188,18 +205,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
);
|
||||
}
|
||||
|
||||
const permissionsEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (permissionsEnabled) {
|
||||
await this.validateSecurityPermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.workspaceRepository.save({
|
||||
...workspace,
|
||||
@ -248,7 +253,10 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
await this.workspaceManagerService.init(workspace.id);
|
||||
await this.workspaceManagerService.init({
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
});
|
||||
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
|
||||
|
||||
await this.workspaceRepository.update(workspace.id, {
|
||||
@ -346,38 +354,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
return !existingWorkspace;
|
||||
}
|
||||
|
||||
private async validateSecurityPermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
payload: Partial<Workspace>;
|
||||
userWorkspaceId?: string;
|
||||
}) {
|
||||
if (
|
||||
isDefined(payload.isGoogleAuthEnabled) ||
|
||||
isDefined(payload.isMicrosoftAuthEnabled) ||
|
||||
isDefined(payload.isPasswordAuthEnabled) ||
|
||||
isDefined(payload.isPublicInviteLinkEnabled)
|
||||
) {
|
||||
if (!userWorkspaceId) {
|
||||
throw new Error('Missing userWorkspaceId in authContext');
|
||||
}
|
||||
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
_setting: SettingsFeatures.SECURITY,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkCustomDomainValidRecords(workspace: Workspace) {
|
||||
if (!workspace.customDomain) return;
|
||||
|
||||
@ -398,4 +374,68 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
|
||||
return customDomainDetails;
|
||||
}
|
||||
|
||||
private async validateSecurityPermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
payload: Partial<Workspace>;
|
||||
userWorkspaceId?: string;
|
||||
}) {
|
||||
if (
|
||||
'isGoogleAuthEnabled' in payload ||
|
||||
'isMicrosoftAuthEnabled' in payload ||
|
||||
'isPasswordAuthEnabled' in payload ||
|
||||
'isPublicInviteLinkEnabled' in payload
|
||||
) {
|
||||
if (!userWorkspaceId) {
|
||||
throw new Error('Missing userWorkspaceId in authContext');
|
||||
}
|
||||
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
_setting: SettingsFeatures.SECURITY,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async validateWorkspacePermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
payload: Partial<Workspace>;
|
||||
userWorkspaceId?: string;
|
||||
}) {
|
||||
if (
|
||||
'displayName' in payload ||
|
||||
'subdomain' in payload ||
|
||||
'customDomain' in payload ||
|
||||
'logo' in payload
|
||||
) {
|
||||
if (!userWorkspaceId) {
|
||||
throw new Error('Missing userWorkspaceId in authContext');
|
||||
}
|
||||
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
_setting: SettingsFeatures.WORKSPACE,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import assert from 'assert';
|
||||
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined, SettingsFeatures } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
@ -43,6 +43,7 @@ import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-worksp
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
@ -120,7 +121,10 @@ export class WorkspaceResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionsGuard(SettingsFeatures.WORKSPACE),
|
||||
)
|
||||
async uploadWorkspaceLogo(
|
||||
@AuthWorkspace() { id }: Workspace,
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
@ -161,7 +165,10 @@ export class WorkspaceResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => Workspace)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionsGuard(SettingsFeatures.WORKSPACE),
|
||||
)
|
||||
async deleteCurrentWorkspace(@AuthWorkspace() { id }: Workspace) {
|
||||
return this.workspaceService.deleteWorkspace(id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user