diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 368e8226a..14e6a7aef 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1,5 +1,6 @@ /* eslint-disable */ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import { PermissionsOnAllObjectRecords } from 'twenty-shared'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -2050,6 +2051,7 @@ export type UserWorkspace = { deletedAt?: Maybe; id: Scalars['UUID']['output']; settingsPermissions?: Maybe>; + objectRecordsPermissions?: Maybe>; updatedAt: Scalars['DateTime']['output']; user: User; userId: Scalars['String']['output']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index bbb8e87c2..98b07b530 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -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 | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -1172,6 +1172,13 @@ export type PageInfo = { startCursor?: Maybe; }; +export enum PermissionsOnAllObjectRecords { + DESTROY_ALL_OBJECT_RECORDS = 'DESTROY_ALL_OBJECT_RECORDS', + READ_ALL_OBJECT_RECORDS = 'READ_ALL_OBJECT_RECORDS', + SOFT_DELETE_ALL_OBJECT_RECORDS = 'SOFT_DELETE_ALL_OBJECT_RECORDS', + UPDATE_ALL_OBJECT_RECORDS = 'UPDATE_ALL_OBJECT_RECORDS' +} + export type PostgresCredentials = { __typename?: 'PostgresCredentials'; id: Scalars['UUID']; @@ -1428,6 +1435,10 @@ export type ResendEmailVerificationTokenOutput = { export type Role = { __typename?: 'Role'; + canDestroyAllObjectRecords: Scalars['Boolean']; + canReadAllObjectRecords: Scalars['Boolean']; + canSoftDeleteAllObjectRecords: Scalars['Boolean']; + canUpdateAllObjectRecords: Scalars['Boolean']; canUpdateAllSettings: Scalars['Boolean']; description?: Maybe; id: Scalars['String']; @@ -1827,6 +1838,7 @@ export type UserWorkspace = { createdAt: Scalars['DateTime']; deletedAt?: Maybe; id: Scalars['UUID']; + objectRecordsPermissions?: Maybe>; settingsPermissions?: Maybe>; updatedAt: Scalars['DateTime']; user: User; @@ -2292,7 +2304,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key: export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2309,7 +2321,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -2582,6 +2594,7 @@ export const UserQueryFragmentFragmentDoc = gql` } currentUserWorkspace { settingsPermissions + objectRecordsPermissions } currentWorkspace { id diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index d3f360324..2b48ef5d4 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -1,5 +1,5 @@ import { gql } from '@apollo/client'; -import { FieldMetadataType } from '~/generated/graphql'; +import { FieldMetadataType, PermissionsOnAllObjectRecords } from '~/generated/graphql'; export const FIELD_METADATA_ID = '2c43466a-fe9e-4005-8d08-c5836067aa6c'; export const FIELD_RELATION_METADATA_ID = @@ -147,6 +147,7 @@ export const queries = { } currentUserWorkspace { settingsPermissions + objectRecordsPermissions } currentWorkspace { id @@ -310,6 +311,12 @@ export const responseData = { workspaceMembers: [], currentUserWorkspace: { settingsPermissions: ['DATA_MODEL'], + objectRecordsPermissions: [ + PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS, + PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS, + PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS, + PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS, + ], }, currentWorkspace: { id: 'test-workspace-id', diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index ea208d649..ded855dc9 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -26,6 +26,7 @@ export const USER_QUERY_FRAGMENT = gql` } currentUserWorkspace { settingsPermissions + objectRecordsPermissions } currentWorkspace { id diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/demo/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/demo/feature-flags.ts index 8b6e51cd5..752345e19 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/demo/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/demo/feature-flags.ts @@ -2,20 +2,6 @@ import { DataSource } from 'typeorm'; const tableName = 'featureFlag'; -// export const seedFeatureFlags = async ( -// workspaceDataSource: DataSource, -// schemaName: string, -// workspaceId: string, -// ) => { -// await workspaceDataSource -// .createQueryBuilder() -// .insert() -// .into(`${schemaName}.${tableName}`, ['key', 'workspaceId', 'value']) -// .orIgnore() -// .values([]) -// .execute(); -// }; - export const deleteFeatureFlags = async ( workspaceDataSource: DataSource, schemaName: string, diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index 349b49097..b337c1df6 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -80,6 +80,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: false, }, + { + key: FeatureFlagKey.IsPermissionsEnabled, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/user-workspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/user-workspaces.ts index b044fd0aa..fe22be544 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/user-workspaces.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/user-workspaces.ts @@ -13,7 +13,7 @@ export const DEV_SEED_USER_WORKSPACE_IDS = { TIM: '20202020-9e3b-46d4-a556-88b9ddc2b035', JONY: '20202020-3957-4908-9c36-2929a23f8353', PHIL: '20202020-7169-42cf-bc47-1cfef15264b1', - TIM_ACME: '20202020-9e3b-46d4-a556-88b9ddc2b436', + TIM_ACME: '20202020-e10a-4c27-a90b-b08c57b02d44', }; export const seedUserWorkspaces = async ( diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1739795699972-updateRoleTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1739795699972-updateRoleTable.ts new file mode 100644 index 000000000..30a1f0153 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1739795699972-updateRoleTable.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateRoleTable1739795699972 implements MigrationInterface { + name = 'UpdateRoleTable1739795699972'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."role" ADD "canReadAllObjectRecords" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."role" ADD "canUpdateAllObjectRecords" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."role" ADD "canSoftDeleteAllObjectRecords" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."role" ADD "canDestroyAllObjectRecords" boolean NOT NULL DEFAULT false`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."role" DROP COLUMN "canDestroyAllObjectRecords"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."role" DROP COLUMN "canSoftDeleteAllObjectRecords"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."role" DROP COLUMN "canUpdateAllObjectRecords"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."role" DROP COLUMN "canReadAllObjectRecords"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index 00976986c..0fc0f5190 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -199,6 +199,7 @@ export abstract class GraphqlQueryBaseResolverService< await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId: authContext.userWorkspaceId, _setting: permissionRequired, + workspaceId: authContext.workspace.id, }); if (!userHasPermission) { diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts index eca6227d6..6872f4c5d 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts @@ -1,7 +1,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; -import { SettingsFeatures } from 'twenty-shared'; +import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared'; import { Column, CreateDateColumn, @@ -25,6 +25,10 @@ registerEnumType(SettingsFeatures, { name: 'SettingsFeatures', }); +registerEnumType(PermissionsOnAllObjectRecords, { + name: 'PermissionsOnAllObjectRecords', +}); + @Entity({ name: 'userWorkspace', schema: 'core' }) @ObjectType() @Unique('IndexOnUserIdAndWorkspaceIdUnique', ['userId', 'workspaceId']) @@ -75,4 +79,7 @@ export class UserWorkspace { @Field(() => [SettingsFeatures], { nullable: true }) settingsPermissions?: SettingsFeatures[]; + + @Field(() => [PermissionsOnAllObjectRecords], { nullable: true }) + objectRecordsPermissions?: PermissionsOnAllObjectRecords[]; } diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index ad7db350a..037bb2853 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -13,7 +13,7 @@ import crypto from 'crypto'; import { GraphQLJSONObject } from 'graphql-type-json'; import { FileUpload, GraphQLUpload } from 'graphql-upload'; -import { SettingsFeatures } from 'twenty-shared'; +import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared'; import { In, Repository } from 'typeorm'; import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; @@ -113,16 +113,23 @@ export class UserResolver { if (!currentUserWorkspace) { throw new Error('Current user workspace not found'); } - const permissions = - await this.permissionsService.getUserWorkspaceSettingsPermissions({ + const { settingsPermissions, objectRecordsPermissions } = + await this.permissionsService.getUserWorkspacePermissions({ userWorkspaceId: currentUserWorkspace.id, + workspaceId: workspace.id, }); const permittedFeatures: SettingsFeatures[] = ( - Object.keys(permissions) as SettingsFeatures[] - ).filter((feature) => permissions[feature] === true); + Object.keys(settingsPermissions) as SettingsFeatures[] + ).filter((feature) => settingsPermissions[feature] === true); + + const permittedObjectRecordsPermissions = ( + Object.keys(objectRecordsPermissions) as PermissionsOnAllObjectRecords[] + ).filter((permission) => objectRecordsPermissions[permission] === true); currentUserWorkspace.settingsPermissions = permittedFeatures; + currentUserWorkspace.objectRecordsPermissions = + permittedObjectRecordsPermissions; user.currentUserWorkspace = currentUserWorkspace; } @@ -216,9 +223,12 @@ export class UserResolver { ); rolesByUserWorkspaces = - await this.userRoleService.getRolesByUserWorkspaces( - userWorkspaces.map((userWorkspace) => userWorkspace.id), - ); + await this.userRoleService.getRolesByUserWorkspaces({ + userWorkspaceIds: userWorkspaces.map( + (userWorkspace) => userWorkspace.id, + ), + workspaceId: workspace.id, + }); } for (const workspaceMemberEntity of workspaceMemberEntities) { @@ -254,6 +264,11 @@ export class UserResolver { description: roleEntity.description, isEditable: roleEntity.isEditable, userWorkspaceRoles: roleEntity.userWorkspaceRoles, + canReadAllObjectRecords: roleEntity.canReadAllObjectRecords, + canUpdateAllObjectRecords: roleEntity.canUpdateAllObjectRecords, + canSoftDeleteAllObjectRecords: + roleEntity.canSoftDeleteAllObjectRecords, + canDestroyAllObjectRecords: roleEntity.canDestroyAllObjectRecords, }; }); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index e41bbac49..6cb7756fa 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -152,11 +152,13 @@ export class WorkspaceService extends TypeOrmQueryService { await this.validateSecurityPermissions({ payload, userWorkspaceId, + workspaceId: workspace.id, }); await this.validateWorkspacePermissions({ payload, userWorkspaceId, + workspaceId: workspace.id, }); } @@ -378,9 +380,11 @@ export class WorkspaceService extends TypeOrmQueryService { private async validateSecurityPermissions({ payload, userWorkspaceId, + workspaceId, }: { payload: Partial; userWorkspaceId?: string; + workspaceId: string; }) { if ( 'isGoogleAuthEnabled' in payload || @@ -396,6 +400,7 @@ export class WorkspaceService extends TypeOrmQueryService { await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, _setting: SettingsFeatures.SECURITY, + workspaceId: workspaceId, }); if (!userHasPermission) { @@ -410,9 +415,11 @@ export class WorkspaceService extends TypeOrmQueryService { private async validateWorkspacePermissions({ payload, userWorkspaceId, + workspaceId, }: { payload: Partial; userWorkspaceId?: string; + workspaceId: string; }) { if ( 'displayName' in payload || @@ -427,6 +434,7 @@ export class WorkspaceService extends TypeOrmQueryService { const userHasPermission = await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, + workspaceId, _setting: SettingsFeatures.WORKSPACE, }); diff --git a/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts b/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts index 977668ca4..01251682e 100644 --- a/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts +++ b/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts @@ -47,6 +47,7 @@ export const SettingsPermissionsGuard = ( await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, _setting: requiredPermission, + workspaceId, }); if (hasPermission === true) { diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts index 4d6f5c912..c5d9a143b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { SettingsFeatures } from 'twenty-shared'; +import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; @@ -12,13 +12,21 @@ export class PermissionsService { private readonly userRoleService: UserRoleService, ) {} - public async getUserWorkspaceSettingsPermissions({ + public async getUserWorkspacePermissions({ userWorkspaceId, + workspaceId, }: { userWorkspaceId: string; - }): Promise> { + workspaceId: string; + }): Promise<{ + settingsPermissions: Record; + objectRecordsPermissions: Record; + }> { const [roleOfUserWorkspace] = await this.userRoleService - .getRolesByUserWorkspaces([userWorkspaceId]) + .getRolesByUserWorkspaces({ + userWorkspaceIds: [userWorkspaceId], + workspaceId, + }) .then((roles) => roles?.get(userWorkspaceId) ?? []); let hasPermissionOnSettingFeature = false; @@ -27,24 +35,48 @@ export class PermissionsService { hasPermissionOnSettingFeature = true; } - return Object.keys(SettingsFeatures).reduce( + const settingsPermissionsMap = Object.keys(SettingsFeatures).reduce( (acc, feature) => ({ ...acc, [feature]: hasPermissionOnSettingFeature, }), {} as Record, ); + + const objectRecordsPermissionsMap: Record< + PermissionsOnAllObjectRecords, + boolean + > = { + [PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace?.canReadAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace?.canUpdateAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace?.canSoftDeleteAllObjectRecords ?? false, + [PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: + roleOfUserWorkspace?.canDestroyAllObjectRecords ?? false, + }; + + return { + settingsPermissions: settingsPermissionsMap, + objectRecordsPermissions: objectRecordsPermissionsMap, + }; } public async userHasWorkspaceSettingPermission({ userWorkspaceId, + workspaceId, _setting, }: { userWorkspaceId: string; + workspaceId: string; _setting: SettingsFeatures; }): Promise { const [roleOfUserWorkspace] = await this.userRoleService - .getRolesByUserWorkspaces([userWorkspaceId]) + .getRolesByUserWorkspaces({ + userWorkspaceIds: [userWorkspaceId], + workspaceId, + }) .then((roles) => roles?.get(userWorkspaceId) ?? []); if (roleOfUserWorkspace?.canUpdateAllSettings === true) { diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts index 5636a6a6d..2e68b82ff 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts @@ -13,9 +13,6 @@ export class RoleDTO { @Field({ nullable: false }) label: string; - @Field({ nullable: false }) - canUpdateAllSettings: boolean; - @Field({ nullable: true }) description: string; @@ -27,4 +24,19 @@ export class RoleDTO { @Field(() => [WorkspaceMember], { nullable: true }) workspaceMembers?: WorkspaceMember[]; + + @Field({ nullable: false }) + canUpdateAllSettings: boolean; + + @Field({ nullable: false }) + canReadAllObjectRecords: boolean; + + @Field({ nullable: false }) + canUpdateAllObjectRecords: boolean; + + @Field({ nullable: false }) + canSoftDeleteAllObjectRecords: boolean; + + @Field({ nullable: false }) + canDestroyAllObjectRecords: boolean; } diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts index 355c81529..7873e17d5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts @@ -21,6 +21,18 @@ export class RoleEntity { @Column({ nullable: false, default: false }) canUpdateAllSettings: boolean; + @Column({ nullable: false, default: false }) + canReadAllObjectRecords: boolean; + + @Column({ nullable: false, default: false }) + canUpdateAllObjectRecords: boolean; + + @Column({ nullable: false, default: false }) + canSoftDeleteAllObjectRecords: boolean; + + @Column({ nullable: false, default: false }) + canDestroyAllObjectRecords: boolean; + @Column({ nullable: true, type: 'text' }) description: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts index f50f39de3..a766817d8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts @@ -38,13 +38,17 @@ export class RoleResolver { return roles.map((role) => ({ id: role.id, label: role.label, - canUpdateAllSettings: role.canUpdateAllSettings, description: role.description, workspaceId: role.workspaceId, createdAt: role.createdAt, updatedAt: role.updatedAt, isEditable: role.isEditable, userWorkspaceRoles: role.userWorkspaceRoles, + canUpdateAllSettings: role.canUpdateAllSettings, + canReadAllObjectRecords: role.canReadAllObjectRecords, + canUpdateAllObjectRecords: role.canUpdateAllObjectRecords, + canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords, + canDestroyAllObjectRecords: role.canDestroyAllObjectRecords, })); } @@ -81,7 +85,10 @@ export class RoleResolver { } const roles = await this.userRoleService - .getRolesByUserWorkspaces([userWorkspace.id]) + .getRolesByUserWorkspaces({ + userWorkspaceIds: [userWorkspace.id], + workspaceId: workspace.id, + }) .then( (rolesByUserWorkspaces) => rolesByUserWorkspaces?.get(userWorkspace.id) ?? [], diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts index c1129ca75..d161681ec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts @@ -30,6 +30,10 @@ export class RoleService { label: ADMIN_ROLE_LABEL, description: 'Admin role', canUpdateAllSettings: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, isEditable: false, workspaceId, }); @@ -44,6 +48,10 @@ export class RoleService { label: MEMBER_ROLE_LABEL, description: 'Member role', canUpdateAllSettings: false, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, isEditable: false, workspaceId, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index 6bd3dc59e..2547b4fe9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -60,7 +60,10 @@ export class UserRoleService { ); } - const roles = await this.getRolesByUserWorkspaces([userWorkspace.id]); + const roles = await this.getRolesByUserWorkspaces({ + userWorkspaceIds: [userWorkspace.id], + workspaceId, + }); const currentRole = roles.get(userWorkspace.id)?.[0]; @@ -88,8 +91,10 @@ export class UserRoleService { workspaceId: string; }): Promise { await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow( - userWorkspaceId, - workspaceId, + { + userWorkspaceId, + workspaceId, + }, ); await this.userWorkspaceRoleRepository.delete({ @@ -98,9 +103,13 @@ export class UserRoleService { }); } - public async getRolesByUserWorkspaces( - userWorkspaceIds: string[], - ): Promise> { + public async getRolesByUserWorkspaces({ + userWorkspaceIds, + workspaceId, + }: { + userWorkspaceIds: string[]; + workspaceId: string; + }): Promise> { if (!userWorkspaceIds.length) { return new Map(); } @@ -108,6 +117,7 @@ export class UserRoleService { const allUserWorkspaceRoles = await this.userWorkspaceRoleRepository.find({ where: { userWorkspaceId: In(userWorkspaceIds), + workspaceId, }, relations: { role: true, @@ -176,11 +186,17 @@ export class UserRoleService { return workspaceMembers; } - private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow( - userWorkspaceId: string, - workspaceId: string, - ): Promise { - const roles = await this.getRolesByUserWorkspaces([userWorkspaceId]); + private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow({ + userWorkspaceId, + workspaceId, + }: { + userWorkspaceId: string; + workspaceId: string; + }): Promise { + const roles = await this.getRolesByUserWorkspaces({ + userWorkspaceIds: [userWorkspaceId], + workspaceId, + }); const currentRoles = roles.get(userWorkspaceId); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 2a678b072..f0d74ee01 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -4,6 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { DEV_SEED_USER_WORKSPACE_IDS } from 'src/database/typeorm-seeds/core/user-workspaces'; +import { + SEED_ACME_WORKSPACE_ID, + SEED_APPLE_WORKSPACE_ID, +} from 'src/database/typeorm-seeds/core/workspaces'; +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 { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -42,6 +47,7 @@ export class WorkspaceManagerService { private readonly userWorkspaceRepository: Repository, private readonly roleService: RoleService, private readonly userRoleService: UserRoleService, + private readonly featureFlagService: FeatureFlagService, ) {} /** @@ -261,20 +267,34 @@ export class WorkspaceManagerService { workspaceId, }); - await this.userRoleService.assignRoleToUserWorkspace({ - workspaceId, - userWorkspaceId: DEV_SEED_USER_WORKSPACE_IDS.TIM, - roleId: adminRole.id, - }); + let adminUserWorkspaceId: string | undefined; + let memberUserWorkspaceId: string | undefined; + + if (workspaceId === SEED_APPLE_WORKSPACE_ID) { + adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM; + memberUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.JONY; + } else if (workspaceId === SEED_ACME_WORKSPACE_ID) { + adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM_ACME; + } + + if (adminUserWorkspaceId) { + await this.userRoleService.assignRoleToUserWorkspace({ + workspaceId, + userWorkspaceId: adminUserWorkspaceId, + roleId: adminRole.id, + }); + } const memberRole = await this.roleService.createMemberRole({ workspaceId, }); - await this.userRoleService.assignRoleToUserWorkspace({ - workspaceId, - userWorkspaceId: DEV_SEED_USER_WORKSPACE_IDS.JONY, - roleId: memberRole.id, - }); + if (memberUserWorkspaceId) { + await this.userRoleService.assignRoleToUserWorkspace({ + workspaceId, + userWorkspaceId: memberUserWorkspaceId, + roleId: memberRole.id, + }); + } } } diff --git a/packages/twenty-shared/src/constants/PermissionsOnAllObjectRecords.ts b/packages/twenty-shared/src/constants/PermissionsOnAllObjectRecords.ts new file mode 100644 index 000000000..cff186f3b --- /dev/null +++ b/packages/twenty-shared/src/constants/PermissionsOnAllObjectRecords.ts @@ -0,0 +1,6 @@ +export enum PermissionsOnAllObjectRecords { + READ_ALL_OBJECT_RECORDS = 'READ_ALL_OBJECT_RECORDS', + UPDATE_ALL_OBJECT_RECORDS = 'UPDATE_ALL_OBJECT_RECORDS', + SOFT_DELETE_ALL_OBJECT_RECORDS = 'SOFT_DELETE_ALL_OBJECT_RECORDS', + DESTROY_ALL_OBJECT_RECORDS = 'DESTROY_ALL_OBJECT_RECORDS', +} diff --git a/packages/twenty-shared/src/constants/index.ts b/packages/twenty-shared/src/constants/index.ts index e1f5f0dd4..10e872cf4 100644 --- a/packages/twenty-shared/src/constants/index.ts +++ b/packages/twenty-shared/src/constants/index.ts @@ -1,4 +1,5 @@ export * from './FieldForTotalCountAggregateOperation'; +export * from './PermissionsOnAllObjectRecords'; export * from './SettingsFeatures'; export * from './TwentyCompaniesBaseUrl'; export * from './TwentyIconsBaseUrl';