[permissions] Add permission gates on API & Webhooks + Security settings (#10133)

Closes https://github.com/twentyhq/core-team-issues/issues/312
Closes https://github.com/twentyhq/core-team-issues/issues/315
This commit is contained in:
Marie
2025-02-12 10:40:26 +01:00
committed by GitHub
parent 08fd227049
commit e4ae76ac20
16 changed files with 220 additions and 44 deletions

View File

@ -45,6 +45,7 @@ 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.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';
@ -89,6 +90,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
EmailVerificationModule,
GuardRedirectModule,
HealthModule,
PermissionsModule,
],
controllers: [
GoogleAuthController,

View File

@ -5,10 +5,12 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { AuthResolver } from './auth.resolver';
@ -85,6 +87,14 @@ describe('AuthResolver', () => {
provide: EmailVerificationTokenService,
useValue: {},
},
{
provide: PermissionsService,
useValue: {},
},
{
provide: FeatureFlagService,
useValue: {},
},
// {
// provide: OAuthService,
// useValue: {},

View File

@ -2,7 +2,7 @@ import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { SOURCE_LOCALE } from 'twenty-shared';
import { SettingsFeatures, SOURCE_LOCALE } from 'twenty-shared';
import { Repository } from 'typeorm';
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
@ -43,8 +43,10 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
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';
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
@ -58,7 +60,7 @@ import { WorkspaceInviteHashValidInput } from './dto/workspace-invite-hash.input
import { AuthService } from './services/auth.service';
@Resolver()
@UseFilters(AuthGraphqlApiExceptionFilter)
@UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter)
export class AuthResolver {
constructor(
@InjectRepository(User, 'core')
@ -323,7 +325,10 @@ export class AuthResolver {
return { tokens: tokens };
}
@UseGuards(WorkspaceAuthGuard)
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionsGuard(SettingsFeatures.API_KEYS_AND_WEBHOOKS),
)
@Mutation(() => ApiKeyToken)
async generateApiKeyToken(
@Args() args: ApiKeyTokenInput,

View File

@ -6,14 +6,15 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { SSOResolver } from 'src/engine/core-modules/sso/sso.resolver';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@Module({
imports: [
NestjsQueryTypeOrmModule.forFeature(
@ -23,6 +24,8 @@ import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guar
BillingModule,
DomainManagerModule,
GuardRedirectModule,
PermissionsModule,
FeatureFlagModule,
],
exports: [SSOService],
providers: [SSOService, SSOResolver],

View File

@ -1,9 +1,10 @@
/* @license Enterprise */
import { UseGuards } from '@nestjs/common';
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import omit from 'lodash.omit';
import { SettingsFeatures } from 'twenty-shared';
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
import { DeleteSsoInput } from 'src/engine/core-modules/sso/dtos/delete-sso.input';
@ -22,9 +23,13 @@ import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { SSOException } from 'src/engine/core-modules/sso/sso.exception';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@Resolver()
@UseFilters(PermissionsGraphqlApiExceptionFilter)
@UseGuards(SettingsPermissionsGuard(SettingsFeatures.SECURITY))
export class SSOResolver {
constructor(private readonly sSOService: SSOService) {}

View File

@ -6,6 +6,7 @@ import { BillingService } from 'src/engine/core-modules/billing/services/billing
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@ -14,8 +15,8 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service'
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { WorkspaceService } from './workspace.service';
@ -86,6 +87,10 @@ describe('WorkspaceService', () => {
provide: ExceptionHandlerService,
useValue: {},
},
{
provide: PermissionsService,
useValue: {},
},
],
}).compile();

View File

@ -4,13 +4,20 @@ import { InjectRepository } from '@nestjs/typeorm';
import assert from 'assert';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { isDefined, WorkspaceActivationStatus } from 'twenty-shared';
import {
isDefined,
SettingsFeatures,
WorkspaceActivationStatus,
} from 'twenty-shared';
import { Repository } from 'typeorm';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.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';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@ -22,10 +29,14 @@ import {
WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@ -47,6 +58,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private readonly environmentService: EnvironmentService,
private readonly domainManagerService: DomainManagerService,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly permissionsService: PermissionsService,
) {
super(workspaceRepository);
}
@ -114,7 +126,13 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
}
}
async updateWorkspaceById(payload: Partial<Workspace> & { id: string }) {
async updateWorkspaceById({
payload,
userWorkspaceId,
}: {
payload: Partial<Workspace> & { id: string };
userWorkspaceId?: string;
}) {
const workspace = await this.workspaceRepository.findOneBy({
id: payload.id,
});
@ -141,6 +159,18 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
customDomainRegistered = true;
}
const permissionsEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
workspace.id,
);
if (permissionsEnabled) {
await this.validateSecurityPermissions({
payload,
userWorkspaceId,
});
}
try {
return await this.workspaceRepository.save({
...workspace,
@ -258,4 +288,36 @@ 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,
);
}
}
}
}

View File

@ -19,6 +19,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
@ -49,6 +50,7 @@ import { WorkspaceService } from './services/workspace.service';
DataSourceModule,
OnboardingModule,
TypeORMModule,
PermissionsModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,

View File

@ -17,6 +17,7 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { CustomDomainDetails } from 'src/engine/core-modules/domain-manager/dtos/custom-domain-details';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
@ -32,26 +33,30 @@ import {
PublicWorkspaceDataOutput,
} from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
import { workspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
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 { 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';
import { GraphqlValidationExceptionFilter } from 'src/filters/graphql-validation-exception.filter';
import { assert } from 'src/utils/assert';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { CustomDomainDetails } from 'src/engine/core-modules/domain-manager/dtos/custom-domain-details';
import { workspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
import { Workspace } from './workspace.entity';
import { WorkspaceService } from './services/workspace.service';
@Resolver(() => Workspace)
@UseFilters(GraphqlValidationExceptionFilter)
@UseFilters(
GraphqlValidationExceptionFilter,
PermissionsGraphqlApiExceptionFilter,
)
export class WorkspaceResolver {
constructor(
private readonly workspaceService: WorkspaceService,
@ -98,11 +103,15 @@ export class WorkspaceResolver {
async updateWorkspace(
@Args('data') data: UpdateWorkspaceInput,
@AuthWorkspace() workspace: Workspace,
@AuthUserWorkspaceId() userWorkspaceId: string,
) {
try {
return await this.workspaceService.updateWorkspaceById({
...data,
id: workspace.id,
payload: {
...data,
id: workspace.id,
},
userWorkspaceId,
});
} catch (error) {
workspaceGraphqlApiExceptionHandler(error);