diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 0774b969a6..87a0a8c63f 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 5b36573bce..924635ceef 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 86ac25e5ba..36c20fcc45 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 b6137a56e8..8c187b5d68 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 84f3d47a7f..1ad9a1e2fe 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 c12ca77fc1..ae868467d0 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 367289de5e..8c2d3421ac 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 46a5a4fbbb..35bea8576e 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 41fe16b483..b5cdead9e2 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 8003f42b21..042067feb9 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 0000000000..24d7099029 --- /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 a9f141fa98..27b0bab717 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 2bc8aadbb2..58416a50ac 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 f6ed283a15..1ca91b2e59 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 375314a428..7fc721050a 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 a509772530..a036db0a64 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 97c935148f..c450aa43ee 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 29d7309b66..05c39aedaa 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 41b7dfd3f9..10fe0b22ae 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 b1ef62ff9a..0714ad914a 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 84abedd86a..69dab2db63 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 a3b8a0a11f..cc2a84ccd6 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 53c1175400..add96582a4 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 41a4b9bc04..701036084b 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 1c6a812057..a8e0821a99 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 006cb53c1f..22108b5747 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 8a7a2e80ab..9940728929 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 0000000000..4f02edb549 --- /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 0000000000..203fc36917 --- /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 f5771a58de..0c607246ce 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 0000000000..addf997bc6 --- /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 0000000000..4c3bd1ba1f --- /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 d6561f6d6e..415e34dac1 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 35677a7c2e..ddd2f4fbfb 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 bb26c55cfc..797046c49c 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 86f9d31799..a04436025d 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 155788e9fd..cad31d82ca 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 5e7f00746b..c8669f63ea 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 c27510abe1..4487d657c7 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 652ea7fb2c..4c0d9275f6 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 3658f4ba85..6d7a5b4090 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 a07a2036f3..e4ac7e2b20 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 0000000000..ce401f5de8 --- /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 0000000000..8458311964 --- /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 bc4af19169..e463baec20 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 0000000000..bd06603905 --- /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 0000000000..8d426be5bf --- /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 beafc6b48d..6f9e3ab940 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 c1082a7b8c..08b645db00 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, + ); + }); + }); + }); + }); });