[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:
@ -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,
|
||||
|
||||
@ -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: {},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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) {}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user