diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 0774b969a..87a0a8c63 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 5b36573bc..924635cee 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 86ac25e5b..36c20fcc4 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 b6137a56e..8c187b5d6 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 84f3d47a7..1ad9a1e2f 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 c12ca77fc..ae868467d 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 367289de5..8c2d3421a 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 46a5a4fbb..35bea8576 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 41fe16b48..b5cdead9e 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 8003f42b2..042067feb 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 000000000..24d709902 --- /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 a9f141fa9..27b0bab71 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 2bc8aadbb..58416a50a 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 f6ed283a1..1ca91b2e5 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 375314a42..7fc721050 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 a50977253..a036db0a6 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 97c935148..c450aa43e 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 29d7309b6..05c39aeda 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 41b7dfd3f..10fe0b22a 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 b1ef62ff9..0714ad914 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 84abedd86..69dab2db6 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 a3b8a0a11..cc2a84ccd 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 53c117540..add96582a 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 41a4b9bc0..701036084 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 1c6a81205..a8e0821a9 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 006cb53c1..22108b574 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 8a7a2e80a..994072892 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 000000000..4f02edb54 --- /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 000000000..203fc3691 --- /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 f5771a58d..0c607246c 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 000000000..addf997bc --- /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 000000000..4c3bd1ba1 --- /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 d6561f6d6..415e34dac 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 35677a7c2..ddd2f4fbf 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 bb26c55cf..797046c49 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 86f9d3179..a04436025 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 155788e9f..cad31d82c 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 5e7f00746..c8669f63e 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 c27510abe..4487d657c 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 652ea7fb2..4c0d9275f 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 3658f4ba8..6d7a5b409 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 a07a2036f..e4ac7e2b2 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 000000000..ce401f5de --- /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 000000000..845831196 --- /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 bc4af1916..e463baec2 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 000000000..bd0660390 --- /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 000000000..8d426be5b --- /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 beafc6b48..6f9e3ab94 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 c1082a7b8..08b645db0 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, + ); + }); + }); + }); + }); });