[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:
@ -1,5 +1,5 @@
|
||||
import * as Apollo from '@apollo/client';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
@ -488,7 +488,6 @@ export type Field = {
|
||||
label: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
object?: Maybe<Object>;
|
||||
objectMetadataId: Scalars['UUID'];
|
||||
options?: Maybe<Scalars['JSON']>;
|
||||
relation?: Maybe<Relation>;
|
||||
relationDefinition?: Maybe<RelationDefinition>;
|
||||
@ -520,7 +519,6 @@ export type FieldFilter = {
|
||||
isActive?: InputMaybe<BooleanFieldComparison>;
|
||||
isCustom?: InputMaybe<BooleanFieldComparison>;
|
||||
isSystem?: InputMaybe<BooleanFieldComparison>;
|
||||
objectMetadataId?: InputMaybe<StringFieldComparison>;
|
||||
or?: InputMaybe<Array<FieldFilter>>;
|
||||
};
|
||||
|
||||
@ -1073,6 +1071,7 @@ export type Object = {
|
||||
dataSourceId: Scalars['String'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
fields: ObjectFieldsConnection;
|
||||
fieldsList: Array<Field>;
|
||||
icon?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
|
||||
@ -1535,7 +1534,7 @@ export enum SettingsFeatures {
|
||||
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
|
||||
DATA_MODEL = 'DATA_MODEL',
|
||||
ROLES = 'ROLES',
|
||||
SECURITY_SETTINGS = 'SECURITY_SETTINGS',
|
||||
SECURITY = 'SECURITY',
|
||||
WORKSPACE_SETTINGS = 'WORKSPACE_SETTINGS',
|
||||
WORKSPACE_USERS = 'WORKSPACE_USERS'
|
||||
}
|
||||
@ -1571,23 +1570,6 @@ export type SignUpOutput = {
|
||||
workspace: WorkspaceUrlsAndId;
|
||||
};
|
||||
|
||||
export type StringFieldComparison = {
|
||||
eq?: InputMaybe<Scalars['String']>;
|
||||
gt?: InputMaybe<Scalars['String']>;
|
||||
gte?: InputMaybe<Scalars['String']>;
|
||||
iLike?: InputMaybe<Scalars['String']>;
|
||||
in?: InputMaybe<Array<Scalars['String']>>;
|
||||
is?: InputMaybe<Scalars['Boolean']>;
|
||||
isNot?: InputMaybe<Scalars['Boolean']>;
|
||||
like?: InputMaybe<Scalars['String']>;
|
||||
lt?: InputMaybe<Scalars['String']>;
|
||||
lte?: InputMaybe<Scalars['String']>;
|
||||
neq?: InputMaybe<Scalars['String']>;
|
||||
notILike?: InputMaybe<Scalars['String']>;
|
||||
notIn?: InputMaybe<Array<Scalars['String']>>;
|
||||
notLike?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum SubscriptionInterval {
|
||||
Day = 'Day',
|
||||
Month = 'Month',
|
||||
@ -1709,7 +1691,6 @@ export type UpdateFieldInput = {
|
||||
isUnique?: InputMaybe<Scalars['Boolean']>;
|
||||
label?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
objectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
options?: InputMaybe<Scalars['JSON']>;
|
||||
settings?: InputMaybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
@ -70,7 +70,13 @@ export class GraphQLConfigService
|
||||
let workspace: Workspace | undefined;
|
||||
|
||||
try {
|
||||
const { user, workspace, apiKey, workspaceMemberId } = context.req;
|
||||
const {
|
||||
user,
|
||||
workspace,
|
||||
apiKey,
|
||||
workspaceMemberId,
|
||||
userWorkspaceId,
|
||||
} = context.req;
|
||||
|
||||
if (!workspace) {
|
||||
return new GraphQLSchema({});
|
||||
@ -81,6 +87,7 @@ export class GraphQLConfigService
|
||||
workspace,
|
||||
apiKey,
|
||||
workspaceMemberId,
|
||||
userWorkspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) {
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
|
||||
export const SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS = {
|
||||
apiKey: SettingsFeatures.API_KEYS_AND_WEBHOOKS,
|
||||
webhook: SettingsFeatures.API_KEYS_AND_WEBHOOKS,
|
||||
} as const;
|
||||
@ -21,6 +21,7 @@ import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-run
|
||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
const graphqlQueryResolvers = [
|
||||
GraphqlQueryCreateManyResolverService,
|
||||
@ -44,6 +45,7 @@ const graphqlQueryResolvers = [
|
||||
WorkspaceQueryHookModule,
|
||||
WorkspaceQueryRunnerModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
],
|
||||
providers: [
|
||||
ApiEventEmitterService,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
import { capitalize, SettingsFeatures } from 'twenty-shared';
|
||||
import { DataSource, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
@ -14,6 +14,7 @@ import {
|
||||
WorkspaceResolverBuilderMethodNames,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant';
|
||||
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||
@ -22,7 +23,18 @@ import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-quer
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
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 {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@ -58,6 +70,8 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
|
||||
@Inject()
|
||||
protected readonly featureFlagService: FeatureFlagService;
|
||||
@Inject()
|
||||
protected readonly permissionsService: PermissionsService;
|
||||
|
||||
public async execute(
|
||||
args: Input,
|
||||
@ -69,6 +83,18 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
|
||||
await this.validate(args, options);
|
||||
|
||||
const permissionsEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsEnabled,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
if (
|
||||
permissionsEnabled === true &&
|
||||
objectMetadataItemWithFieldMaps.isSystem === true
|
||||
) {
|
||||
await this.validateSystemObjectPermissions(options);
|
||||
}
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
@ -146,6 +172,45 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
}
|
||||
}
|
||||
|
||||
private async validateSystemObjectPermissions(
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
) {
|
||||
const { authContext, objectMetadataItemWithFieldMaps } = options;
|
||||
|
||||
if (
|
||||
Object.keys(SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS).includes(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
)
|
||||
) {
|
||||
if (!authContext.apiKey) {
|
||||
if (!authContext.userWorkspaceId) {
|
||||
throw new AuthException(
|
||||
'Missing userWorkspaceId in authContext',
|
||||
AuthExceptionCode.USER_WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const permissionRequired: SettingsFeatures =
|
||||
SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS[
|
||||
objectMetadataItemWithFieldMaps.nameSingular
|
||||
];
|
||||
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
_setting: permissionRequired,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<Input>,
|
||||
): Promise<Response>;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -8,13 +8,25 @@ export class PermissionsException extends CustomException {
|
||||
}
|
||||
|
||||
export enum PermissionsExceptionCode {
|
||||
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
||||
ADMIN_ROLE_NOT_FOUND = 'ADMIN_ROLE_NOT_FOUND',
|
||||
USER_WORKSPACE_NOT_FOUND = 'USER_WORKSPACE_NOT_FOUND',
|
||||
WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH = 'WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH',
|
||||
TOO_MANY_ADMIN_CANDIDATES = 'TOO_MANY_ADMIN_CANDIDATES',
|
||||
USER_WORKSPACE_ALREADY_HAS_ROLE = 'USER_WORKSPACE_ALREADY_HAS_ROLE',
|
||||
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
||||
WORKSPACE_MEMBER_NOT_FOUND = 'WORKSPACE_MEMBER_NOT_FOUND',
|
||||
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
||||
CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN',
|
||||
}
|
||||
|
||||
export enum PermissionsExceptionMessage {
|
||||
PERMISSION_DENIED = 'User does not have permission',
|
||||
ADMIN_ROLE_NOT_FOUND = 'Admin role not found',
|
||||
USER_WORKSPACE_NOT_FOUND = 'User workspace not found',
|
||||
WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH = 'Workspace id role user workspace mismatch',
|
||||
TOO_MANY_ADMIN_CANDIDATES = 'Too many admin candidates',
|
||||
USER_WORKSPACE_ALREADY_HAS_ROLE = 'User workspace already has role',
|
||||
WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found',
|
||||
ROLE_NOT_FOUND = 'Role not found',
|
||||
CANNOT_UNASSIGN_LAST_ADMIN = 'Cannot unassign last admin',
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
export enum SettingsFeatures {
|
||||
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
|
||||
WORKSPACE_SETTINGS = 'WORKSPACE_SETTINGS',
|
||||
WORKSPACE = 'WORKSPACE',
|
||||
WORKSPACE_USERS = 'WORKSPACE_USERS',
|
||||
ROLES = 'ROLES',
|
||||
DATA_MODEL = 'DATA_MODEL',
|
||||
ADMIN_PANEL = 'ADMIN_PANEL',
|
||||
SECURITY_SETTINGS = 'SECURITY_SETTINGS',
|
||||
SECURITY = 'SECURITY',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user