From 4680bc740a14cb969710c7aeeff0fe2cfc8879d3 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:07:51 +0100 Subject: [PATCH] =?UTF-8?q?[permissions=20V2]=C2=A0Upsert=20object=20and?= =?UTF-8?q?=20setting=20permissions=20(#11119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/twentyhq/core-team-issues/issues/639 --- .../twenty-front/src/generated/graphql.tsx | 54 ++- .../modules/app/components/SettingsRoutes.tsx | 16 +- .../hooks/useRecordGroupActions.ts | 8 +- .../SettingsProtectedRouteWrapper.tsx | 4 +- .../useSettingsNavigationItems.test.tsx | 26 +- .../hooks/useSettingsNavigationItems.tsx | 23 +- .../roles/hooks/useHasSettingsPermission.ts | 13 +- .../roles/hooks/useSettingsPermissionMap.ts | 6 +- .../components/RolePermissions.tsx | 16 +- .../src/testing/mock-data/users.ts | 4 +- .../1742488572894-renamePermissionTables.ts | 39 ++ ...jects-permissions-requirements.constant.ts | 6 +- .../interfaces/base-resolver-service.ts | 8 +- .../engine/core-modules/auth/auth.resolver.ts | 8 +- .../core-modules/billing/billing.resolver.ts | 8 +- .../engine/core-modules/lab/lab.resolver.ts | 4 +- .../engine/core-modules/sso/sso.resolver.ts | 4 +- .../user-workspace/user-workspace.entity.ts | 12 +- .../engine/core-modules/user/user.resolver.ts | 8 +- .../workspace-invitation.resolver.ts | 4 +- .../workspace/services/workspace.service.ts | 8 +- .../workspace/workspace.resolver.ts | 8 +- .../guards/settings-permissions.guard.ts | 4 +- .../field-metadata/field-metadata.resolver.ts | 9 +- .../object-metadata/object-metadata.entity.ts | 13 +- .../object-metadata/object-metadata.module.ts | 6 +- .../object-metadata.resolver.ts | 7 +- .../dtos/object-permission.dto.ts | 25 ++ .../dtos/upsert-object-permission-input.ts | 36 ++ .../object-permission.entity.ts} | 6 +- .../object-permission.module.ts | 19 + .../object-permission.service.ts | 115 ++++++ ...s => setting-permission-type.constants.ts} | 2 +- .../permissions/permissions.exception.ts | 6 + .../permissions/permissions.service.ts | 12 +- ...sion-graphql-api-exception-handler.util.ts | 3 + .../relation-metadata.module.ts | 6 +- .../relation-metadata.resolver.ts | 4 +- ...eInput.dto.ts => create-role-input.dto.ts} | 0 ...eInput.dto.ts => update-role-input.dto.ts} | 0 .../metadata-modules/role/role.entity.ts | 17 +- .../metadata-modules/role/role.module.ts | 4 + .../metadata-modules/role/role.resolver.ts | 114 ++++-- .../metadata-modules/role/role.service.ts | 6 +- .../dtos/setting-permission.dto.ts | 18 + .../dtos/upsert-setting-permission-input.ts | 29 ++ .../setting-permission.entity.ts} | 12 +- .../setting-permission.module.ts | 15 + .../setting-permission.service.ts | 79 +++++ ...workspace-member-pre-query-hook.service.ts | 4 +- .../roles.integration-spec.ts | 332 ++++++++++++++++-- 51 files changed, 985 insertions(+), 205 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1742488572894-renamePermissionTables.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input.ts rename packages/twenty-server/src/engine/metadata-modules/{object-permissions/object-permissions.entity.ts => object-permission/object-permission.entity.ts} (91%) create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts rename packages/twenty-server/src/engine/metadata-modules/permissions/constants/{settings-permissions.constants.ts => setting-permission-type.constants.ts} (86%) rename packages/twenty-server/src/engine/metadata-modules/role/dtos/{createRoleInput.dto.ts => create-role-input.dto.ts} (100%) rename packages/twenty-server/src/engine/metadata-modules/role/dtos/{updateRoleInput.dto.ts => update-role-input.dto.ts} (100%) create mode 100644 packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts rename packages/twenty-server/src/engine/metadata-modules/{settings-permissions/settings-permissions.entity.ts => setting-permission/setting-permission.entity.ts} (68%) create mode 100644 packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 0774b969a6c..87a0a8c63fd 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -865,6 +865,8 @@ export type Mutation = { uploadImage: Scalars['String']; uploadProfilePicture: Scalars['String']; uploadWorkspaceLogo: Scalars['String']; + upsertOneObjectPermission: ObjectPermission; + upsertOneSettingPermission: SettingPermission; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; }; @@ -1164,6 +1166,16 @@ export type MutationUploadWorkspaceLogoArgs = { }; +export type MutationUpsertOneObjectPermissionArgs = { + upsertObjectPermissionInput: UpsertObjectPermissionInput; +}; + + +export type MutationUpsertOneSettingPermissionArgs = { + upsertSettingPermissionInput: UpsertSettingPermissionInput; +}; + + export type MutationUserLookupAdminPanelArgs = { userIdentifier: Scalars['String']; }; @@ -1255,6 +1267,17 @@ export type ObjectIndexMetadatasConnection = { pageInfo: PageInfo; }; +export type ObjectPermission = { + __typename?: 'ObjectPermission'; + canDestroyObjectRecords?: Maybe; + canReadObjectRecords?: Maybe; + canSoftDeleteObjectRecords?: Maybe; + canUpdateObjectRecords?: Maybe; + id: Scalars['String']; + objectMetadataId: Scalars['String']; + roleId: Scalars['String']; +}; + export type ObjectRecordFilterInput = { and?: InputMaybe>; createdAt?: InputMaybe; @@ -1715,7 +1738,15 @@ export enum ServerlessFunctionSyncStatus { READY = 'READY' } -export enum SettingsPermissions { +export type SettingPermission = { + __typename?: 'SettingPermission'; + canUpdateSetting?: Maybe; + id: Scalars['String']; + roleId: Scalars['String']; + setting: SettingPermissionType; +}; + +export enum SettingPermissionType { ADMIN_PANEL = 'ADMIN_PANEL', API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', DATA_MODEL = 'DATA_MODEL', @@ -1988,6 +2019,21 @@ export type UpdateWorkspaceInput = { subdomain?: InputMaybe; }; +export type UpsertObjectPermissionInput = { + canDestroyObjectRecords?: InputMaybe; + canReadObjectRecords?: InputMaybe; + canSoftDeleteObjectRecords?: InputMaybe; + canUpdateObjectRecords?: InputMaybe; + objectMetadataId: Scalars['String']; + roleId: Scalars['String']; +}; + +export type UpsertSettingPermissionInput = { + canUpdateSetting?: InputMaybe; + roleId: Scalars['String']; + setting: SettingPermissionType; +}; + export type User = { __typename?: 'User'; analyticsTinybirdJwts?: Maybe; @@ -2062,7 +2108,7 @@ export type UserWorkspace = { deletedAt?: Maybe; id: Scalars['UUID']; objectRecordsPermissions?: Maybe>; - settingsPermissions?: Maybe>; + settingsPermissions?: Maybe>; updatedAt: Scalars['DateTime']; user: User; userId: Scalars['String']; @@ -2607,7 +2653,7 @@ export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never export type GetSsoIdentityProvidersQuery = { __typename?: 'Query', getSSOIdentityProviders: 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, canAccessFullAdminPanel: boolean, 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, isCustomDomainEnabled: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | 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, canAccessFullAdminPanel: boolean, 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, isCustomDomainEnabled: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | 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; }>; @@ -2624,7 +2670,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, canAccessFullAdminPanel: boolean, 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, isCustomDomainEnabled: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | 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, canAccessFullAdminPanel: boolean, 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, isCustomDomainEnabled: boolean, 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 }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | 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']; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 5b36573bcee..924635ceef4 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -5,7 +5,7 @@ import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsPro import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { SettingsPath } from '@/types/SettingsPath'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; -import { SettingsPermissions } from '~/generated/graphql'; +import { SettingPermissionType } from '~/generated/graphql'; const SettingsApiKeys = lazy(() => import('~/pages/settings/developers/api-keys/SettingsApiKeys').then( @@ -334,7 +334,7 @@ export const SettingsRoutes = ({ } > @@ -345,7 +345,7 @@ export const SettingsRoutes = ({ } > @@ -357,7 +357,7 @@ export const SettingsRoutes = ({ } > @@ -387,7 +387,7 @@ export const SettingsRoutes = ({ } @@ -398,7 +398,7 @@ export const SettingsRoutes = ({ } > @@ -465,7 +465,7 @@ export const SettingsRoutes = ({ } > @@ -496,7 +496,7 @@ export const SettingsRoutes = ({ } > diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts index 86ac25e5baa..36c20fcc455 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts @@ -12,10 +12,10 @@ import { ViewType } from '@/views/types/ViewType'; import { useCallback, useContext } from 'react'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; -import { IconEyeOff, IconSettings } from 'twenty-ui'; -import { SettingsPermissions } from '~/generated/graphql'; -import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { isDefined } from 'twenty-shared/utils'; +import { IconEyeOff, IconSettings } from 'twenty-ui'; +import { SettingPermissionType } from '~/generated/graphql'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; type UseRecordGroupActionsParams = { viewType: ViewType; @@ -71,7 +71,7 @@ export const useRecordGroupActions = ({ ]); const hasAccessToDataModelSettings = useHasSettingsPermission( - SettingsPermissions.DATA_MODEL, + SettingPermissionType.DATA_MODEL, ); const recordGroupActions: RecordGroupAction[] = []; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx b/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx index b6137a56e8d..8c187b5d680 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx @@ -3,12 +3,12 @@ import { SettingsPath } from '@/types/SettingsPath'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { ReactNode } from 'react'; import { Navigate, Outlet } from 'react-router-dom'; -import { FeatureFlagKey, SettingsPermissions } from '~/generated/graphql'; +import { FeatureFlagKey, SettingPermissionType } from '~/generated/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; type SettingsProtectedRouteWrapperProps = { children?: ReactNode; - settingsPermission?: SettingsPermissions; + settingsPermission?: SettingPermissionType; requiredFeatureFlag?: FeatureFlagKey; }; diff --git a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx index 84f3d47a7f1..1ad9a1e2fe9 100644 --- a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx @@ -8,7 +8,7 @@ import { Billing, FeatureFlagKey, OnboardingStatus, - SettingsPermissions, + SettingPermissionType, } from '~/generated/graphql'; import { currentUserState } from '@/auth/states/currentUserState'; @@ -61,12 +61,12 @@ jest.mock('@/workspace/hooks/useFeatureFlagsMap', () => ({ describe('useSettingsNavigationItems', () => { it('should hide workspace settings when no permissions', () => { (useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({ - [SettingsPermissions.WORKSPACE]: false, - [SettingsPermissions.WORKSPACE_MEMBERS]: false, - [SettingsPermissions.DATA_MODEL]: false, - [SettingsPermissions.API_KEYS_AND_WEBHOOKS]: false, - [SettingsPermissions.ROLES]: false, - [SettingsPermissions.SECURITY]: false, + [SettingPermissionType.WORKSPACE]: false, + [SettingPermissionType.WORKSPACE_MEMBERS]: false, + [SettingPermissionType.DATA_MODEL]: false, + [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: false, + [SettingPermissionType.ROLES]: false, + [SettingPermissionType.SECURITY]: false, })); const { result } = renderHook(() => useSettingsNavigationItems(), { @@ -82,12 +82,12 @@ describe('useSettingsNavigationItems', () => { it('should show workspace settings when has permissions', () => { (useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({ - [SettingsPermissions.WORKSPACE]: true, - [SettingsPermissions.WORKSPACE_MEMBERS]: true, - [SettingsPermissions.DATA_MODEL]: true, - [SettingsPermissions.API_KEYS_AND_WEBHOOKS]: true, - [SettingsPermissions.ROLES]: true, - [SettingsPermissions.SECURITY]: true, + [SettingPermissionType.WORKSPACE]: true, + [SettingPermissionType.WORKSPACE_MEMBERS]: true, + [SettingPermissionType.DATA_MODEL]: true, + [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: true, + [SettingPermissionType.ROLES]: true, + [SettingPermissionType.SECURITY]: true, })); const { result } = renderHook(() => useSettingsNavigationItems(), { diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx index c12ca77fc1f..ae868467d09 100644 --- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx @@ -22,7 +22,6 @@ import { import { SettingsPath } from '@/types/SettingsPath'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; -import { SettingsPermissions } from '~/generated/graphql'; import { currentUserState } from '@/auth/states/currentUserState'; import { billingState } from '@/client-config/states/billingState'; @@ -32,6 +31,7 @@ import { NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { t } from '@lingui/core/macro'; import { useRecoilValue } from 'recoil'; +import { SettingPermissionType } from '~/generated/graphql'; export type SettingsNavigationSection = { label: string; @@ -108,13 +108,13 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { label: t`General`, path: SettingsPath.Workspace, Icon: IconSettings, - isHidden: !permissionMap[SettingsPermissions.WORKSPACE], + isHidden: !permissionMap[SettingPermissionType.WORKSPACE], }, { label: t`Members`, path: SettingsPath.WorkspaceMembersPage, Icon: IconUsers, - isHidden: !permissionMap[SettingsPermissions.WORKSPACE_MEMBERS], + isHidden: !permissionMap[SettingPermissionType.WORKSPACE_MEMBERS], }, { label: t`Roles`, @@ -122,33 +122,34 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { Icon: IconLock, isHidden: !featureFlags[FeatureFlagKey.IsPermissionsEnabled] || - !permissionMap[SettingsPermissions.ROLES], + !permissionMap[SettingPermissionType.ROLES], }, { label: t`Billing`, path: SettingsPath.Billing, Icon: IconCurrencyDollar, isHidden: - !isBillingEnabled || !permissionMap[SettingsPermissions.WORKSPACE], + !isBillingEnabled || + !permissionMap[SettingPermissionType.WORKSPACE], }, { label: t`Data model`, path: SettingsPath.Objects, Icon: IconHierarchy2, - isHidden: !permissionMap[SettingsPermissions.DATA_MODEL], + isHidden: !permissionMap[SettingPermissionType.DATA_MODEL], }, { label: t`Integrations`, path: SettingsPath.Integrations, Icon: IconApps, - isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS], + isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS], }, { label: t`Security`, path: SettingsPath.Security, Icon: IconKey, isAdvanced: true, - isHidden: !permissionMap[SettingsPermissions.SECURITY], + isHidden: !permissionMap[SettingPermissionType.SECURITY], }, ], }, @@ -161,14 +162,14 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { path: SettingsPath.APIs, Icon: IconApi, isAdvanced: true, - isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS], + isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS], }, { label: t`Webhooks`, path: SettingsPath.Webhooks, Icon: IconWebhook, isAdvanced: true, - isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS], + isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS], }, { label: t`Functions`, @@ -194,7 +195,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { Icon: IconFlask, isHidden: !labPublicFeatureFlags.length || - !permissionMap[SettingsPermissions.WORKSPACE], + !permissionMap[SettingPermissionType.WORKSPACE], }, { label: t`Releases`, diff --git a/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts b/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts index 367289de5e3..8c2d3421ac2 100644 --- a/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts +++ b/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts @@ -1,11 +1,11 @@ import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useRecoilValue } from 'recoil'; -import { SettingsPermissions } from '~/generated/graphql'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { SettingPermissionType } from '~/generated/graphql'; export const useHasSettingsPermission = ( - settingsPermission?: SettingsPermissions, + settingsPermission?: SettingPermissionType, ) => { const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState); @@ -15,19 +15,18 @@ export const useHasSettingsPermission = ( } if ( - settingsPermission === SettingsPermissions.WORKSPACE && + settingsPermission === SettingPermissionType.WORKSPACE && currentWorkspace?.activationStatus === WorkspaceActivationStatus.PENDING_CREATION ) { return true; } - const currentUserWorkspaceSettingsPermissions = - currentUserWorkspace?.settingsPermissions; + const currentUserWorkspaceSetting = currentUserWorkspace?.settingsPermissions; - if (!currentUserWorkspaceSettingsPermissions) { + if (!currentUserWorkspaceSetting) { return false; } - return currentUserWorkspaceSettingsPermissions.includes(settingsPermission); + return currentUserWorkspaceSetting.includes(settingsPermission); }; diff --git a/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts b/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts index 46a5a4fbbb1..35bea8576e5 100644 --- a/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts +++ b/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts @@ -2,11 +2,11 @@ import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceSta import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useRecoilValue } from 'recoil'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; -import { SettingsPermissions } from '~/generated/graphql'; +import { SettingPermissionType } from '~/generated/graphql'; import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue'; export const useSettingsPermissionMap = (): Record< - SettingsPermissions, + SettingPermissionType, boolean > => { const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState); @@ -19,7 +19,7 @@ export const useSettingsPermissionMap = (): Record< currentUserWorkspace?.settingsPermissions; const initialPermissions = buildRecordFromKeysWithSameValue( - Object.values(SettingsPermissions), + Object.values(SettingPermissionType), !isPermissionEnabled, ); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissions.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissions.tsx index 41fe16b483d..b5cdead9e26 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissions.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/RolePermissions.tsx @@ -21,7 +21,7 @@ import { Section, } from 'twenty-ui'; import { Role } from '~/generated-metadata/graphql'; -import { SettingsPermissions } from '~/generated/graphql'; +import { SettingPermissionType } from '~/generated/graphql'; import { RolePermissionsObjectsTableRow } from './RolePermissionsObjectsTableRow'; const StyledRolePermissionsContainer = styled.div` @@ -81,49 +81,49 @@ export const RolePermissions = ({ role }: RolePermissionsProps) => { const settingsPermissionsConfig: RolePermissionsSettingPermission[] = [ { - key: SettingsPermissions.API_KEYS_AND_WEBHOOKS, + key: SettingPermissionType.API_KEYS_AND_WEBHOOKS, name: 'API Keys & Webhooks', description: 'Manage API keys and webhooks', value: role.canUpdateAllSettings, Icon: IconCode, }, { - key: SettingsPermissions.WORKSPACE, + key: SettingPermissionType.WORKSPACE, name: 'Workspace', description: 'Set global workspace preferences', value: role.canUpdateAllSettings, Icon: IconSettings, }, { - key: SettingsPermissions.WORKSPACE_MEMBERS, + key: SettingPermissionType.WORKSPACE_MEMBERS, name: 'Users', description: 'Add or remove users', value: role.canUpdateAllSettings, Icon: IconUsers, }, { - key: SettingsPermissions.ROLES, + key: SettingPermissionType.ROLES, name: 'Roles', description: 'Define user roles and access levels', value: role.canUpdateAllSettings, Icon: IconLockOpen, }, { - key: SettingsPermissions.DATA_MODEL, + key: SettingPermissionType.DATA_MODEL, name: 'Data Model', description: 'Edit CRM data structure and fields', value: role.canUpdateAllSettings, Icon: IconHierarchy, }, { - key: SettingsPermissions.ADMIN_PANEL, + key: SettingPermissionType.ADMIN_PANEL, name: 'Admin Panel', description: 'Admin settings and system tools', value: role.canUpdateAllSettings, Icon: IconServer, }, { - key: SettingsPermissions.SECURITY, + key: SettingPermissionType.SECURITY, name: 'Security', description: 'Manage security policies', value: role.canUpdateAllSettings, diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 8003f42b21d..042067feb91 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -3,7 +3,7 @@ import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { FeatureFlagKey, OnboardingStatus, - SettingsPermissions, + SettingPermissionType, SubscriptionInterval, SubscriptionStatus, User, @@ -131,7 +131,7 @@ export const mockedUserData: MockedUser = { workspaceMember: mockedWorkspaceMemberData, currentWorkspace: mockCurrentWorkspace, currentUserWorkspace: { - settingsPermissions: [SettingsPermissions.WORKSPACE_MEMBERS], + settingsPermissions: [SettingPermissionType.WORKSPACE_MEMBERS], }, locale: 'en', workspaces: [{ workspace: mockCurrentWorkspace }], diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1742488572894-renamePermissionTables.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1742488572894-renamePermissionTables.ts new file mode 100644 index 00000000000..24d70990296 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1742488572894-renamePermissionTables.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenamePermissionTables1742488572894 implements MigrationInterface { + name = 'RenamePermissionTables1742488572894'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "metadata"."settingsPermissions"`); + await queryRunner.query(`DROP TABLE "metadata"."objectPermissions"`); + await queryRunner.query( + `CREATE TABLE "metadata"."objectPermission" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "roleId" uuid NOT NULL, "objectMetadataId" uuid NOT NULL, "canReadObjectRecords" boolean, "canUpdateObjectRecords" boolean, "canSoftDeleteObjectRecords" boolean, "canDestroyObjectRecords" boolean, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "IndexOnObjectPermissionUnique" UNIQUE ("objectMetadataId", "roleId"), CONSTRAINT "PK_23a4033c1aa380d0d1431731add" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "metadata"."settingPermission" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "roleId" uuid NOT NULL, "setting" character varying NOT NULL, "canUpdateSetting" boolean, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "IndexOnSettingPermissionUnique" UNIQUE ("setting", "roleId"), CONSTRAINT "PK_8c144a021030d7e3326835a04c8" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."objectPermission" ADD CONSTRAINT "FK_826052747c82e59f0a006204256" FOREIGN KEY ("roleId") REFERENCES "metadata"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."objectPermission" ADD CONSTRAINT "FK_efbcf3528718de2b5c45c0a8a83" FOREIGN KEY ("objectMetadataId") REFERENCES "metadata"."objectMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."settingPermission" ADD CONSTRAINT "FK_b327aadd9fd189f33d2c5237833" FOREIGN KEY ("roleId") REFERENCES "metadata"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."settingPermission" DROP CONSTRAINT "FK_b327aadd9fd189f33d2c5237833"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."objectPermission" DROP CONSTRAINT "FK_efbcf3528718de2b5c45c0a8a83"`, + ); + await queryRunner.query( + `ALTER TABLE "metadata"."objectPermission" DROP CONSTRAINT "FK_826052747c82e59f0a006204256"`, + ); + await queryRunner.query(`DROP TABLE "metadata"."settingPermission"`); + await queryRunner.query(`DROP TABLE "metadata"."objectPermission"`); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant.ts index a9f141fa98f..27b0bab7179 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant.ts @@ -1,6 +1,6 @@ -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; export const SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS = { - apiKey: SettingsPermissions.API_KEYS_AND_WEBHOOKS, - webhook: SettingsPermissions.API_KEYS_AND_WEBHOOKS, + apiKey: SettingPermissionType.API_KEYS_AND_WEBHOOKS, + webhook: SettingPermissionType.API_KEYS_AND_WEBHOOKS, } as const; 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 2bc8aadbb20..58416a50acb 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 @@ -1,9 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import graphqlFields from 'graphql-fields'; -import { DataSource, ObjectLiteral } from 'typeorm'; -import { capitalize, isDefined } from 'twenty-shared/utils'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; +import { capitalize, isDefined } from 'twenty-shared/utils'; +import { DataSource, ObjectLiteral } from 'typeorm'; import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; @@ -27,7 +27,7 @@ import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-quer import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -190,7 +190,7 @@ export abstract class GraphqlQueryBaseResolverService< objectMetadataItemWithFieldMaps.nameSingular, ) ) { - const permissionRequired: SettingsPermissions = + const permissionRequired: SettingPermissionType = SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS[ objectMetadataItemWithFieldMaps.nameSingular ]; diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index f6ed283a159..1ca91b2e592 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -3,8 +3,8 @@ import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql'; import { InjectRepository } from '@nestjs/typeorm'; import omit from 'lodash.omit'; -import { Repository } from 'typeorm'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; +import { Repository } from 'typeorm'; import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input'; import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input'; @@ -29,6 +29,7 @@ import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dt import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input'; import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output'; import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service'; +import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { RenewTokenService } from 'src/engine/core-modules/auth/token/services/renew-token.service'; @@ -49,9 +50,8 @@ 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input'; import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input'; @@ -367,7 +367,7 @@ export class AuthResolver { @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingsPermissions.API_KEYS_AND_WEBHOOKS), + SettingsPermissionsGuard(SettingPermissionType.API_KEYS_AND_WEBHOOKS), ) @Mutation(() => ApiKeyToken) async generateApiKeyToken( diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index 375314a4286..7fc721050ad 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -28,7 +28,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -52,7 +52,7 @@ export class BillingResolver { @Query(() => BillingSessionOutput) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), + SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), ) async billingPortalSession( @AuthWorkspace() workspace: Workspace, @@ -115,7 +115,7 @@ export class BillingResolver { @Mutation(() => BillingUpdateOutput) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), + SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), ) async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) { await this.billingSubscriptionService.applyBillingSubscription(workspace); @@ -161,7 +161,7 @@ export class BillingResolver { await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, workspaceId, - _setting: SettingsPermissions.WORKSPACE, + _setting: SettingPermissionType.WORKSPACE, isExecutedByApiKey, }); diff --git a/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts b/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts index a5097725301..a036db0a644 100644 --- a/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts @@ -11,12 +11,12 @@ 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; @Resolver() @UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter) -@UseGuards(SettingsPermissionsGuard(SettingsPermissions.WORKSPACE)) +@UseGuards(SettingsPermissionsGuard(SettingPermissionType.WORKSPACE)) export class LabResolver { constructor(private featureFlagService: FeatureFlagService) {} diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts index 97c935148f1..c450aa43ee4 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts @@ -20,12 +20,12 @@ 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; @Resolver() @UseFilters(PermissionsGraphqlApiExceptionFilter) -@UseGuards(SettingsPermissionsGuard(SettingsPermissions.SECURITY)) +@UseGuards(SettingsPermissionsGuard(SettingPermissionType.SECURITY)) export class SSOResolver { constructor(private readonly sSOService: SSOService) {} 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 29d7309b665..05c39aedaa0 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,6 +1,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; import { Column, CreateDateColumn, @@ -14,16 +15,15 @@ import { Unique, UpdateDateColumn, } from 'typeorm'; -import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; -registerEnumType(SettingsPermissions, { - name: 'SettingsPermissions', +registerEnumType(SettingPermissionType, { + name: 'SettingPermissionType', }); registerEnumType(PermissionsOnAllObjectRecords, { @@ -78,8 +78,8 @@ export class UserWorkspace { ) twoFactorMethods: Relation; - @Field(() => [SettingsPermissions], { nullable: true }) - settingsPermissions?: SettingsPermissions[]; + @Field(() => [SettingPermissionType], { nullable: true }) + settingsPermissions?: SettingPermissionType[]; @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 41b7dfd3f9b..10fe0b22aee 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,8 +13,8 @@ import crypto from 'crypto'; import { GraphQLJSONObject } from 'graphql-type-json'; import { FileUpload, GraphQLUpload } from 'graphql-upload'; -import { In, Repository } from 'typeorm'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; +import { In, Repository } from 'typeorm'; import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; @@ -48,7 +48,7 @@ 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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; @@ -122,8 +122,8 @@ export class UserResolver { workspaceId: workspace.id, }); - const grantedSettingsPermissions: SettingsPermissions[] = ( - Object.keys(settingsPermissions) as SettingsPermissions[] + const grantedSettingsPermissions: SettingPermissionType[] = ( + Object.keys(settingsPermissions) as SettingPermissionType[] ).filter((feature) => settingsPermissions[feature] === true); const grantedObjectRecordsPermissions = ( diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts index b1ef62ff9a7..0714ad914a9 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts @@ -12,14 +12,14 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { SendInvitationsInput } from './dtos/send-invitations.input'; @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingsPermissions.WORKSPACE_MEMBERS), + SettingsPermissionsGuard(SettingPermissionType.WORKSPACE_MEMBERS), ) @UseFilters(PermissionsGraphqlApiExceptionFilter) @Resolver() 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 84abedd86a9..69dab2db635 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 @@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm'; import assert from 'assert'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +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'; @@ -34,7 +34,7 @@ import { WorkspaceExceptionCode, } from 'src/engine/core-modules/workspace/workspace.exception'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -442,7 +442,7 @@ export class WorkspaceService extends TypeOrmQueryService { const userHasPermission = await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, - _setting: SettingsPermissions.SECURITY, + _setting: SettingPermissionType.SECURITY, workspaceId: workspaceId, isExecutedByApiKey: isDefined(apiKey), }); @@ -481,7 +481,7 @@ export class WorkspaceService extends TypeOrmQueryService { await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, workspaceId, - _setting: SettingsPermissions.WORKSPACE, + _setting: SettingPermissionType.WORKSPACE, isExecutedByApiKey: isDefined(apiKey), }); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index a3b8a0a11fd..cc2a84ccd68 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -12,8 +12,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import assert from 'assert'; import { FileUpload, GraphQLUpload } from 'graphql-upload'; -import { Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; @@ -47,7 +47,7 @@ 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; @@ -130,7 +130,7 @@ export class WorkspaceResolver { @Mutation(() => String) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), + SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), ) async uploadWorkspaceLogo( @AuthWorkspace() { id }: Workspace, @@ -174,7 +174,7 @@ export class WorkspaceResolver { @Mutation(() => Workspace) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), + SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), ) async deleteCurrentWorkspace(@AuthWorkspace() { id }: Workspace) { return this.workspaceService.deleteWorkspace(id); 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 53c11754006..add96582a43 100644 --- a/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts +++ b/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts @@ -11,7 +11,7 @@ import { isDefined } from 'twenty-shared/utils'; 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -20,7 +20,7 @@ import { import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; export const SettingsPermissionsGuard = ( - requiredPermission: SettingsPermissions, + requiredPermission: SettingPermissionType, ): Type => { @Injectable() class SettingsPermissionsMixin implements CanActivate { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts index 41a4b9bc043..701036084be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts @@ -36,7 +36,7 @@ import { import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; @@ -74,6 +74,7 @@ export class FieldMetadataResolver { ); } + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @ResolveField(() => String, { nullable: true }) async icon( @Parent() fieldMetadata: FieldMetadataDTO, @@ -86,7 +87,7 @@ export class FieldMetadataResolver { ); } - @UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @Mutation(() => FieldMetadataDTO) async createOneField( @Args('input') input: CreateOneFieldMetadataInput, @@ -102,7 +103,7 @@ export class FieldMetadataResolver { } } - @UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @Mutation(() => FieldMetadataDTO) async updateOneField( @Args('input') input: UpdateOneFieldMetadataInput, @@ -123,7 +124,7 @@ export class FieldMetadataResolver { } } - @UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @Mutation(() => FieldMetadataDTO) async deleteOneField( @Args('input') input: DeleteOneFieldInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index 1c6a8120574..a8e0821a999 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -17,7 +17,7 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto'; -import { ObjectPermissionsEntity } from 'src/engine/metadata-modules/object-permissions/object-permissions.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; @Entity('objectMetadata') @@ -142,9 +142,12 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface { updatedAt: Date; @OneToMany( - () => ObjectPermissionsEntity, - (objectPermissions: ObjectPermissionsEntity) => - objectPermissions.objectMetadata, + () => ObjectPermissionEntity, + (objectPermission: ObjectPermissionEntity) => + objectPermission.objectMetadata, + { + cascade: true, + }, ) - objectPermissions: Relation; + objectPermissions: Relation; } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 006cb53c1fb..22108b5747f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -22,7 +22,7 @@ import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metad import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; @@ -78,7 +78,9 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; }, create: { many: { disabled: true }, - guards: [SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)], + guards: [ + SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL), + ], }, update: { disabled: true }, delete: { disabled: true }, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts index 8a7a2e80ab6..9940728929f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts @@ -24,7 +24,7 @@ import { import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; @UseGuards(WorkspaceAuthGuard) @@ -72,6 +72,7 @@ export class ObjectMetadataResolver { ); } + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @ResolveField(() => String, { nullable: true }) async icon( @Parent() objectMetadata: ObjectMetadataDTO, @@ -84,7 +85,7 @@ export class ObjectMetadataResolver { ); } - @UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async deleteOneObject( @Args('input') input: DeleteOneObjectInput, @@ -100,7 +101,7 @@ export class ObjectMetadataResolver { } } - @UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async updateOneObject( @Args('input') input: UpdateOneObjectInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts new file mode 100644 index 00000000000..4f02edb5497 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/object-permission.dto.ts @@ -0,0 +1,25 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType('ObjectPermission') +export class ObjectPermissionDTO { + @Field({ nullable: false }) + id: string; + + @Field({ nullable: false }) + roleId: string; + + @Field({ nullable: false }) + objectMetadataId: string; + + @Field({ nullable: true }) + canReadObjectRecords?: boolean; + + @Field({ nullable: true }) + canUpdateObjectRecords?: boolean; + + @Field({ nullable: true }) + canSoftDeleteObjectRecords?: boolean; + + @Field({ nullable: true }) + canDestroyObjectRecords?: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input.ts new file mode 100644 index 00000000000..203fc369179 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input.ts @@ -0,0 +1,36 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; + +@InputType() +export class UpsertObjectPermissionInput { + @IsUUID() + @IsNotEmpty() + @Field() + roleId: string; + + @IsUUID() + @IsNotEmpty() + @Field() + objectMetadataId: string; + + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + canReadObjectRecords?: boolean; + + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + canUpdateObjectRecords?: boolean; + + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + canSoftDeleteObjectRecords?: boolean; + + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + canDestroyObjectRecords?: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permissions/object-permissions.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.entity.ts similarity index 91% rename from packages/twenty-server/src/engine/metadata-modules/object-permissions/object-permissions.entity.ts rename to packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.entity.ts index f5771a58de5..0c607246cea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permissions/object-permissions.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.entity.ts @@ -13,9 +13,9 @@ import { import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -@Entity('objectPermissions') -@Unique('IndexOnObjectPermissionsUnique', ['objectMetadataId', 'roleId']) -export class ObjectPermissionsEntity { +@Entity('objectPermission') +@Unique('IndexOnObjectPermissionUnique', ['objectMetadataId', 'roleId']) +export class ObjectPermissionEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts new file mode 100644 index 00000000000..addf997bc62 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature( + [ObjectPermissionEntity, RoleEntity, ObjectMetadataEntity], + 'metadata', + ), + ], + providers: [ObjectPermissionService], + exports: [ObjectPermissionService], +}) +export class ObjectPermissionModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts new file mode 100644 index 00000000000..4c3bd1ba1f6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts @@ -0,0 +1,115 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { + PermissionsException, + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; + +export class ObjectPermissionService { + constructor( + @InjectRepository(ObjectPermissionEntity, 'metadata') + private readonly objectPermissionRepository: Repository, + @InjectRepository(RoleEntity, 'metadata') + private readonly roleRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + public async upsertObjectPermission({ + workspaceId, + input, + }: { + workspaceId: string; + input: UpsertObjectPermissionInput; + }): Promise { + try { + const result = await this.objectPermissionRepository.upsert( + { + workspaceId, + ...input, + }, + { + conflictPaths: ['objectMetadataId', 'roleId'], + }, + ); + + const objectPermissionId = result.generatedMaps?.[0]?.id; + + if (!isDefined(objectPermissionId)) { + throw new Error('Failed to upsert object permission'); + } + + return this.objectPermissionRepository.findOne({ + where: { + id: objectPermissionId, + }, + }); + } catch (error) { + await this.handleForeignKeyError({ + error, + roleId: input.roleId, + workspaceId, + objectMetadataId: input.objectMetadataId, + }); + + throw error; + } + } + + private async handleForeignKeyError({ + error, + roleId, + workspaceId, + objectMetadataId, + }: { + error: Error; + roleId: string; + workspaceId: string; + objectMetadataId: string; + }) { + if (error.message.includes('violates foreign key constraint')) { + const role = await this.getRole(roleId, workspaceId); + + if (!isDefined(role)) { + throw new PermissionsException( + PermissionsExceptionMessage.ROLE_NOT_FOUND, + PermissionsExceptionCode.ROLE_NOT_FOUND, + ); + } + + const objectMetadata = await this.objectMetadataRepository.findOne({ + where: { + workspaceId, + id: objectMetadataId, + }, + }); + + if (!isDefined(objectMetadata)) { + throw new PermissionsException( + PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND, + PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, + ); + } + } + } + + private async getRole( + roleId: string, + workspaceId: string, + ): Promise { + return this.roleRepository.findOne({ + where: { + id: roleId, + workspaceId, + }, + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/constants/settings-permissions.constants.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts similarity index 86% rename from packages/twenty-server/src/engine/metadata-modules/permissions/constants/settings-permissions.constants.ts rename to packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts index d6561f6d6e1..415e34dac1f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/constants/settings-permissions.constants.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts @@ -1,4 +1,4 @@ -export enum SettingsPermissions { +export enum SettingPermissionType { API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', WORKSPACE = 'WORKSPACE', WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS', diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts index 35677a7c2ee..ddd2f4fbfbd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts @@ -25,6 +25,9 @@ export enum PermissionsExceptionCode { PERMISSIONS_V2_NOT_ENABLED = 'PERMISSIONS_V2_NOT_ENABLED', ROLE_LABEL_ALREADY_EXISTS = 'ROLE_LABEL_ALREADY_EXISTS', DEFAULT_ROLE_NOT_FOUND = 'DEFAULT_ROLE_NOT_FOUND', + OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', + INVALID_SETTING = 'INVALID_SETTING', + ROLE_NOT_EDITABLE = 'ROLE_NOT_EDITABLE', } export enum PermissionsExceptionMessage { @@ -45,4 +48,7 @@ export enum PermissionsExceptionMessage { PERMISSIONS_V2_NOT_ENABLED = 'Permissions V2 is not enabled', ROLE_LABEL_ALREADY_EXISTS = 'A role with this label already exists', DEFAULT_ROLE_NOT_FOUND = 'Default role not found', + OBJECT_METADATA_NOT_FOUND = 'Object metadata not found', + INVALID_SETTING = 'Invalid permission setting (unknown value)', + ROLE_NOT_EDITABLE = 'Role is not editable', } 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 bb26c55cfce..797046c49c0 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,14 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'twenty-shared/utils'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -31,7 +31,7 @@ export class PermissionsService { userWorkspaceId: string; workspaceId: string; }): Promise<{ - settingsPermissions: Record; + settingsPermissions: Record; objectRecordsPermissions: Record; }> { const [roleOfUserWorkspace] = await this.userRoleService @@ -47,12 +47,12 @@ export class PermissionsService { hasPermissionOnSettingFeature = true; } - const settingsPermissionsMap = Object.keys(SettingsPermissions).reduce( + const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce( (acc, feature) => ({ ...acc, [feature]: hasPermissionOnSettingFeature, }), - {} as Record, + {} as Record, ); const objectRecordsPermissionsMap: Record< @@ -83,7 +83,7 @@ export class PermissionsService { }: { userWorkspaceId?: string; workspaceId: string; - _setting: SettingsPermissions; + _setting: SettingPermissionType; isExecutedByApiKey: boolean; }): Promise { if (isExecutedByApiKey) { diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts index 86f9d317995..a04436025db 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts @@ -19,11 +19,14 @@ export const permissionGraphqlApiExceptionHandler = ( case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER: case PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED: case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS: + case PermissionsExceptionCode.ROLE_NOT_EDITABLE: throw new ForbiddenError(error.message); case PermissionsExceptionCode.INVALID_ARG: + case PermissionsExceptionCode.INVALID_SETTING: throw new UserInputError(error.message); case PermissionsExceptionCode.ROLE_NOT_FOUND: case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND: + case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND: throw new NotFoundError(error.message); case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND: default: diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts index 155788e9fd1..cad31d82ca7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts @@ -13,7 +13,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RelationMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/relation-metadata/interceptors/relation-metadata-graphql-api-exception.interceptor'; @@ -57,7 +57,9 @@ import { RelationMetadataDTO } from './dtos/relation-metadata.dto'; pagingStrategy: PagingStrategies.CURSOR, create: { many: { disabled: true }, - guards: [SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)], + guards: [ + SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL), + ], }, update: { disabled: true }, delete: { disabled: true }, diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.resolver.ts index 5e7f00746bb..c8669f63ea9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.resolver.ts @@ -5,7 +5,7 @@ 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { DeleteOneRelationInput } from 'src/engine/metadata-modules/relation-metadata/dtos/delete-relation.input'; import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto'; @@ -20,7 +20,7 @@ export class RelationMetadataResolver { private readonly relationMetadataService: RelationMetadataService, ) {} - @UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @Mutation(() => RelationMetadataDTO) async deleteOneRelation( @Args('input') input: DeleteOneRelationInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/createRoleInput.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/role/dtos/createRoleInput.dto.ts rename to packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/updateRoleInput.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/role/dtos/updateRoleInput.dto.ts rename to packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts 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 c27510abe10..4487d657c7c 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 @@ -9,9 +9,9 @@ import { UpdateDateColumn, } from 'typeorm'; -import { ObjectPermissionsEntity } from 'src/engine/metadata-modules/object-permissions/object-permissions.entity'; +import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; -import { SettingsPermissionsEntity } from 'src/engine/metadata-modules/settings-permissions/settings-permissions.entity'; +import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity'; @Entity('role') @Unique('IndexOnRoleUnique', ['label', 'workspaceId']) @@ -62,15 +62,14 @@ export class RoleEntity { userWorkspaceRoles: Relation; @OneToMany( - () => ObjectPermissionsEntity, - (objectPermissions: ObjectPermissionsEntity) => objectPermissions.role, + () => ObjectPermissionEntity, + (objectPermission: ObjectPermissionEntity) => objectPermission.role, ) - objectPermissions: Relation; + objectPermissions: Relation; @OneToMany( - () => SettingsPermissionsEntity, - (settingsPermissions: SettingsPermissionsEntity) => - settingsPermissions.role, + () => SettingPermissionEntity, + (settingPermission: SettingPermissionEntity) => settingPermission.role, ) - settingsPermissions: Relation; + settingPermissions: Relation; } diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index 652ea7fb2cc..4c0d9275f63 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -4,11 +4,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; +import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; +import { SettingPermissionModule } from 'src/engine/metadata-modules/setting-permission/setting-permission.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @Module({ @@ -19,6 +21,8 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. PermissionsModule, UserWorkspaceModule, FeatureFlagModule, + ObjectPermissionModule, + SettingPermissionModule, ], providers: [RoleService, RoleResolver], exports: [RoleService], 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 3658f4ba855..6d7a5b4090c 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 @@ -16,22 +16,28 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto'; +import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input'; +import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/createRoleInput.dto'; +import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto'; import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; -import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/updateRoleInput.dto'; +import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; +import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto'; +import { UpsertSettingPermissionInput } from 'src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input'; +import { SettingPermissionService } from 'src/engine/metadata-modules/setting-permission/setting-permission.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Resolver(() => RoleDTO) -@UseGuards(SettingsPermissionsGuard(SettingsPermissions.ROLES)) +@UseGuards(SettingsPermissionsGuard(SettingPermissionType.ROLES)) @UseFilters(PermissionsGraphqlApiExceptionFilter) export class RoleResolver { constructor( @@ -39,6 +45,8 @@ export class RoleResolver { private readonly roleService: RoleService, private readonly userWorkspaceService: UserWorkspaceService, private readonly featureFlagService: FeatureFlagService, + private readonly objectPermissionService: ObjectPermissionService, + private readonly settingPermissionService: SettingPermissionService, ) {} @Query(() => [RoleDTO]) @@ -101,18 +109,7 @@ export class RoleResolver { @AuthWorkspace() workspace: Workspace, @Args('createRoleInput') createRoleInput: CreateRoleInput, ): Promise { - const isPermissionsV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IsPermissionsV2Enabled, - workspace.id, - ); - - if (!isPermissionsV2Enabled) { - throw new PermissionsException( - PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED, - PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED, - ); - } + await this.validatePermissionsV2EnabledOrThrow(workspace); return this.roleService.createRole({ workspaceId: workspace.id, @@ -125,18 +122,11 @@ export class RoleResolver { @AuthWorkspace() workspace: Workspace, @Args('updateRoleInput') updateRoleInput: UpdateRoleInput, ): Promise { - const isPermissionsV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IsPermissionsV2Enabled, - workspace.id, - ); - - if (!isPermissionsV2Enabled) { - throw new PermissionsException( - PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED, - PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED, - ); - } + await this.validatePermissionsV2EnabledOrThrow(workspace); + await this.validateRoleIsEditableOrThrow({ + roleId: updateRoleInput.id, + workspaceId: workspace.id, + }); return this.roleService.updateRole({ input: updateRoleInput, @@ -144,6 +134,42 @@ export class RoleResolver { }); } + @Mutation(() => ObjectPermissionDTO) + async upsertOneObjectPermission( + @AuthWorkspace() workspace: Workspace, + @Args('upsertObjectPermissionInput') + upsertObjectPermissionInput: UpsertObjectPermissionInput, + ) { + await this.validatePermissionsV2EnabledOrThrow(workspace); + await this.validateRoleIsEditableOrThrow({ + roleId: upsertObjectPermissionInput.roleId, + workspaceId: workspace.id, + }); + + return this.objectPermissionService.upsertObjectPermission({ + workspaceId: workspace.id, + input: upsertObjectPermissionInput, + }); + } + + @Mutation(() => SettingPermissionDTO) + async upsertOneSettingPermission( + @AuthWorkspace() workspace: Workspace, + @Args('upsertSettingPermissionInput') + upsertSettingPermissionInput: UpsertSettingPermissionInput, + ) { + await this.validatePermissionsV2EnabledOrThrow(workspace); + await this.validateRoleIsEditableOrThrow({ + roleId: upsertSettingPermissionInput.roleId, + workspaceId: workspace.id, + }); + + return this.settingPermissionService.upsertSettingPermission({ + workspaceId: workspace.id, + input: upsertSettingPermissionInput, + }); + } + @ResolveField('workspaceMembers', () => [WorkspaceMember]) async getWorkspaceMembersAssignedToRole( @Parent() role: RoleDTO, @@ -154,4 +180,36 @@ export class RoleResolver { workspace.id, ); } + + private async validatePermissionsV2EnabledOrThrow(workspace: Workspace) { + const isPermissionsV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsPermissionsV2Enabled, + workspace.id, + ); + + if (!isPermissionsV2Enabled) { + throw new PermissionsException( + PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED, + PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED, + ); + } + } + + private async validateRoleIsEditableOrThrow({ + roleId, + workspaceId, + }: { + roleId: string; + workspaceId: string; + }) { + const role = await this.roleService.getRoleById(roleId, workspaceId); + + if (!role?.isEditable) { + throw new PermissionsException( + PermissionsExceptionMessage.ROLE_NOT_EDITABLE, + PermissionsExceptionCode.ROLE_NOT_EDITABLE, + ); + } + } } 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 a07a2036f38..e4ac7e2b200 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 @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants'; import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants'; @@ -10,11 +10,11 @@ import { PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/createRoleInput.dto'; +import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto'; import { UpdateRoleInput, UpdateRolePayload, -} from 'src/engine/metadata-modules/role/dtos/updateRoleInput.dto'; +} from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts new file mode 100644 index 00000000000..ce401f5de88 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts @@ -0,0 +1,18 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; + +@ObjectType('SettingPermission') +export class SettingPermissionDTO { + @Field({ nullable: false }) + id: string; + + @Field({ nullable: false }) + roleId: string; + + @Field({ nullable: false }) + setting: SettingPermissionType; + + @Field({ nullable: true }) + canUpdateSetting?: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts new file mode 100644 index 00000000000..8458311964c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts @@ -0,0 +1,29 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; + +@InputType() +export class UpsertSettingPermissionInput { + @IsUUID() + @IsNotEmpty() + @Field() + roleId: string; + + @IsString() + @IsNotEmpty() + @Field({ nullable: false }) + setting: SettingPermissionType; + + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + canUpdateSetting?: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/settings-permissions/settings-permissions.entity.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.entity.ts similarity index 68% rename from packages/twenty-server/src/engine/metadata-modules/settings-permissions/settings-permissions.entity.ts rename to packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.entity.ts index bc4af191696..e463baec20c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/settings-permissions/settings-permissions.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.entity.ts @@ -10,26 +10,26 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -@Entity('settingsPermissions') -@Unique('IndexOnSettingsPermissionsUnique', ['setting', 'roleId']) -export class SettingsPermissionsEntity { +@Entity('settingPermission') +@Unique('IndexOnSettingPermissionUnique', ['setting', 'roleId']) +export class SettingPermissionEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column({ nullable: false, type: 'uuid' }) roleId: string; - @ManyToOne(() => RoleEntity, (role) => role.settingsPermissions, { + @ManyToOne(() => RoleEntity, (role) => role.settingPermissions, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'roleId' }) role: Relation; @Column({ nullable: false, type: 'varchar' }) - setting: SettingsPermissions; + setting: SettingPermissionType; @Column({ nullable: true, type: 'boolean' }) canUpdateSetting?: boolean; diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts new file mode 100644 index 00000000000..bd066039055 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity'; +import { SettingPermissionService } from 'src/engine/metadata-modules/setting-permission/setting-permission.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([SettingPermissionEntity, RoleEntity], 'metadata'), + ], + providers: [SettingPermissionService], + exports: [SettingPermissionService], +}) +export class SettingPermissionModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts new file mode 100644 index 00000000000..8d426be5bff --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts @@ -0,0 +1,79 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { + PermissionsException, + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { UpsertSettingPermissionInput } from 'src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input'; +import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity'; + +export class SettingPermissionService { + constructor( + @InjectRepository(SettingPermissionEntity, 'metadata') + private readonly settingPermissionRepository: Repository, + @InjectRepository(RoleEntity, 'metadata') + private readonly roleRepository: Repository, + ) {} + + public async upsertSettingPermission({ + workspaceId, + input, + }: { + workspaceId: string; + input: UpsertSettingPermissionInput; + }): Promise { + if (!Object.values(SettingPermissionType).includes(input.setting)) { + throw new PermissionsException( + PermissionsExceptionMessage.INVALID_SETTING, + PermissionsExceptionCode.INVALID_SETTING, + ); + } + + try { + const result = await this.settingPermissionRepository.upsert( + { + workspaceId, + ...input, + }, + { + conflictPaths: ['setting', 'roleId'], + }, + ); + + const settingPermissionId = result.generatedMaps?.[0]?.id; + + if (!isDefined(settingPermissionId)) { + throw new Error('Failed to upsert setting permission'); + } + + return this.settingPermissionRepository.findOne({ + where: { + id: settingPermissionId, + }, + }); + } catch (error) { + if (error.message.includes('violates foreign key constraint')) { + const role = await this.roleRepository.findOne({ + where: { + id: input.roleId, + }, + }); + + if (!isDefined(role)) { + throw new PermissionsException( + PermissionsExceptionMessage.ROLE_NOT_FOUND, + PermissionsExceptionCode.ROLE_NOT_FOUND, + ); + } + } + + throw error; + } + } +} diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts index beafc6b48d9..6f9e3ab9403 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts @@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; 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 { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -65,7 +65,7 @@ export class WorkspaceMemberPreQueryHookService { await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, workspaceId, - _setting: SettingsPermissions.WORKSPACE_MEMBERS, + _setting: SettingPermissionType.WORKSPACE_MEMBERS, isExecutedByApiKey: isDefined(apiKey), }) ) { diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts index c1082a7b8c7..08b645db00e 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts @@ -1,15 +1,41 @@ import request from 'supertest'; import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util'; +import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util'; +import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces'; import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; const client = request(`http://localhost:${APP_PORT}`); +async function assertPermissionDeniedForMemberWithMemberRole({ + query, +}: { + query: { query: string }; +}) { + await client + .post('/graphql') + .set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`) + .send(query) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeNull(); + expect(res.body.errors).toBeDefined(); + expect(res.body.errors[0].message).toBe( + PermissionsExceptionMessage.PERMISSION_DENIED, + ); + expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN); + }); +} + describe('roles permissions', () => { + let adminRoleId: string; + let guestRoleId: string; + beforeAll(async () => { const enablePermissionsQuery = updateFeatureFlagFactory( SEED_APPLE_WORKSPACE_ID, @@ -17,7 +43,38 @@ describe('roles permissions', () => { true, ); + const enablePermissionsV2Query = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IsPermissionsV2Enabled', + true, + ); + await makeGraphqlAPIRequest(enablePermissionsQuery); + await makeGraphqlAPIRequest(enablePermissionsV2Query); + + const query = { + query: ` + query GetRoles { + getRoles { + label + id + } + } + `, + }; + + const resp = await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(query); + + adminRoleId = resp.body.data.getRoles.find( + (role) => role.label === 'Admin', + ).id; + + guestRoleId = resp.body.data.getRoles.find( + (role) => role.label === 'Guest', + ).id; }); afterAll(async () => { @@ -27,7 +84,14 @@ describe('roles permissions', () => { false, ); + const disablePermissionsV2Query = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IsPermissionsV2Enabled', + false, + ); + await makeGraphqlAPIRequest(disablePermissionsQuery); + await makeGraphqlAPIRequest(disablePermissionsV2Query); }); describe('getRoles', () => { @@ -116,19 +180,7 @@ describe('roles permissions', () => { `, }; - await client - .post('/graphql') - .set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`) - .send(query) - .expect(200) - .expect((res) => { - expect(res.body.data).toBeNull(); - expect(res.body.errors).toBeDefined(); - expect(res.body.errors[0].message).toBe( - PermissionsExceptionMessage.PERMISSION_DENIED, - ); - expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN); - }); + await assertPermissionDeniedForMemberWithMemberRole({ query }); }); }); @@ -144,19 +196,7 @@ describe('roles permissions', () => { `, }; - await client - .post('/graphql') - .set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`) - .send(query) - .expect(200) - .expect((res) => { - expect(res.body.data).toBeNull(); - expect(res.body.errors).toBeDefined(); - expect(res.body.errors[0].message).toBe( - PermissionsExceptionMessage.PERMISSION_DENIED, - ); - expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN); - }); + await assertPermissionDeniedForMemberWithMemberRole({ query }); }); it('should throw a permission error when tries to update their own role (admin role)', async () => { @@ -260,4 +300,244 @@ describe('roles permissions', () => { }); }); }); + + describe('createRole', () => { + it('should throw a permission error when user does not have permission to create roles (member role)', async () => { + const query = { + query: ` + mutation CreateOneRole { + createOneRole(createRoleInput: {label: "test-role"}) { + id + } + } + `, + }; + + await assertPermissionDeniedForMemberWithMemberRole({ query }); + }); + + // TODO - to uncomment after deleteOneRole has been implemented + // it('should create a role when user has permission to create a role (admin role)', async () => { + // const query = { + // query: ` + // mutation CreateOneRole { + // createOneRole(createRoleInput: {label: "Test role"}) { + // id + // } + // } + // `, + // }; + + // await client + // .post('/graphql') + // .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + // .send(query) + // .expect(200) + // .expect((res) => { + // expect(res.body.data).toBeDefined(); + // expect(res.body.errors).toBeUndefined(); + // }); + // }); + }); + + describe('updateRole', () => { + // let createdEditableRoleId: string; + + beforeAll(async () => { + // TODO - to uncomment after deleteOneRole has been implemented + // const query = { + // query: ` + // mutation CreateOneRole { + // createOneRole(createRoleInput: {label: "Test role 2"}) { + // id + // } + // } + // `, + // }; + // await client + // .post('/graphql') + // .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + // .send(query) + // .then((res) => { + // createdEditableRoleId = res.body.data.createOneRole.id; + // }); + }); + + describe('updateRole', () => { + it('should throw a permission error when user does not have permission to update roles (member role)', async () => { + const query = { + query: ` + mutation UpdateOneRole { + updateOneRole(updateRoleInput: {id: "test-role-id", update: {label: "new-role-label"}}) { + id + } + } + `, + }; + + await assertPermissionDeniedForMemberWithMemberRole({ query }); + }); + + it('should throw an error when role is not editable', async () => { + const query = { + query: ` + mutation UpdateOneRole { + updateOneRole(updateRoleInput: {id: "${adminRoleId}", update: {label: "new-role-label"}}) { + id + } + } + `, + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(query) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeNull(); + expect(res.body.errors).toBeDefined(); + expect(res.body.errors[0].message).toBe( + PermissionsExceptionMessage.ROLE_NOT_EDITABLE, + ); + expect(res.body.errors[0].extensions.code).toBe( + ErrorCode.FORBIDDEN, + ); + }); + }); + }); + + describe('upsertObjectPermission', () => { + let listingObjectId = ''; + + beforeAll(async () => { + const { objectMetadataId: createdObjectId } = + await createListingCustomObject(); + + listingObjectId = createdObjectId; + }); + + afterAll(async () => { + await deleteOneObjectMetadataItem(listingObjectId); + }); + + const upsertObjectPermissionMutation = ({ + objectMetadataId, + roleId, + }: { + objectMetadataId: string; + roleId: string; + }) => ` + mutation UpsertObjectPermissions { + upsertOneObjectPermission(upsertObjectPermissionInput: {objectMetadataId: "${objectMetadataId}", roleId: "${roleId}", canUpdateObjectRecords: true}) { + id + } + } + `; + + it('should throw a permission error when user does not have permission to upsert object permission (member role)', async () => { + const query = { + query: upsertObjectPermissionMutation({ + objectMetadataId: listingObjectId, + roleId: guestRoleId, + }), + }; + + await assertPermissionDeniedForMemberWithMemberRole({ query }); + }); + + it('should throw an error when role is not editable', async () => { + const query = { + query: upsertObjectPermissionMutation({ + objectMetadataId: listingObjectId, + roleId: adminRoleId, + }), + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(query) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeNull(); + expect(res.body.errors).toBeDefined(); + expect(res.body.errors[0].message).toBe( + PermissionsExceptionMessage.ROLE_NOT_EDITABLE, + ); + expect(res.body.errors[0].extensions.code).toBe( + ErrorCode.FORBIDDEN, + ); + }); + }); + + // TODO - to uncomment after deleteOneRole has been implemented + // it('should upsert a setting permission when user has permission to create a setting permission', async () => { + // const query = { + // query: upsertObjectPermissionMutation({ + // objectMetadataId: listingObjectId, + // roleId: createdEditableRoleId, + // }), + // }; + + // await client + // .post('/graphql') + // .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + // .send(query) + // .expect(200) + // .expect((res) => { + // expect(res.body.data).toBeDefined(); + // expect(res.body.errors).toBeUndefined(); + // }); + // }); + }); + + describe('upsertSettingPermission', () => { + const upsertSettingPermissionMutation = ({ + roleId, + }: { + roleId: string; + }) => ` + mutation UpsertSettingPermissions { + upsertOneSettingPermission(upsertSettingPermissionInput: {roleId: "${roleId}", setting: ${SettingPermissionType.DATA_MODEL}}) { + id + } + } + `; + + it('should throw a permission error when user does not have permission to upsert object permission (member role)', async () => { + const query = { + query: upsertSettingPermissionMutation({ + roleId: guestRoleId, + }), + }; + + await assertPermissionDeniedForMemberWithMemberRole({ query }); + }); + + it('should throw an error when role is not editable', async () => { + const query = { + query: upsertSettingPermissionMutation({ + roleId: adminRoleId, + }), + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(query) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeNull(); + expect(res.body.errors).toBeDefined(); + expect(res.body.errors[0].message).toBe( + PermissionsExceptionMessage.ROLE_NOT_EDITABLE, + ); + expect(res.body.errors[0].extensions.code).toBe( + ErrorCode.FORBIDDEN, + ); + }); + }); + }); + }); });