[permissions V2] Upsert object and setting permissions (#11119)

Closes https://github.com/twentyhq/core-team-issues/issues/639
This commit is contained in:
Marie
2025-03-25 11:07:51 +01:00
committed by GitHub
parent 54e346a2aa
commit 4680bc740a
51 changed files with 985 additions and 205 deletions

View File

@ -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<Scalars['Boolean']>;
canReadObjectRecords?: Maybe<Scalars['Boolean']>;
canSoftDeleteObjectRecords?: Maybe<Scalars['Boolean']>;
canUpdateObjectRecords?: Maybe<Scalars['Boolean']>;
id: Scalars['String'];
objectMetadataId: Scalars['String'];
roleId: Scalars['String'];
};
export type ObjectRecordFilterInput = {
and?: InputMaybe<Array<ObjectRecordFilterInput>>;
createdAt?: InputMaybe<DateFilter>;
@ -1715,7 +1738,15 @@ export enum ServerlessFunctionSyncStatus {
READY = 'READY'
}
export enum SettingsPermissions {
export type SettingPermission = {
__typename?: 'SettingPermission';
canUpdateSetting?: Maybe<Scalars['Boolean']>;
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<Scalars['String']>;
};
export type UpsertObjectPermissionInput = {
canDestroyObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadObjectRecords?: InputMaybe<Scalars['Boolean']>;
canSoftDeleteObjectRecords?: InputMaybe<Scalars['Boolean']>;
canUpdateObjectRecords?: InputMaybe<Scalars['Boolean']>;
objectMetadataId: Scalars['String'];
roleId: Scalars['String'];
};
export type UpsertSettingPermissionInput = {
canUpdateSetting?: InputMaybe<Scalars['Boolean']>;
roleId: Scalars['String'];
setting: SettingPermissionType;
};
export type User = {
__typename?: 'User';
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
@ -2062,7 +2108,7 @@ export type UserWorkspace = {
deletedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['UUID'];
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
settingsPermissions?: Maybe<Array<SettingsPermissions>>;
settingsPermissions?: Maybe<Array<SettingPermissionType>>;
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<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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'];

View File

@ -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 = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.WORKSPACE}
settingsPermission={SettingPermissionType.WORKSPACE}
/>
}
>
@ -345,7 +345,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.WORKSPACE_MEMBERS}
settingsPermission={SettingPermissionType.WORKSPACE_MEMBERS}
/>
}
>
@ -357,7 +357,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.DATA_MODEL}
settingsPermission={SettingPermissionType.DATA_MODEL}
/>
}
>
@ -387,7 +387,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.ROLES}
settingsPermission={SettingPermissionType.ROLES}
requiredFeatureFlag={FeatureFlagKey.IsPermissionsEnabled}
/>
}
@ -398,7 +398,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.API_KEYS_AND_WEBHOOKS}
settingsPermission={SettingPermissionType.API_KEYS_AND_WEBHOOKS}
/>
}
>
@ -465,7 +465,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.SECURITY}
settingsPermission={SettingPermissionType.SECURITY}
/>
}
>
@ -496,7 +496,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.WORKSPACE}
settingsPermission={SettingPermissionType.WORKSPACE}
/>
}
>

View File

@ -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[] = [];

View File

@ -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;
};

View File

@ -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(), {

View File

@ -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`,

View File

@ -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);
};

View File

@ -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,
);

View File

@ -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,

View File

@ -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 }],

View File

@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RenamePermissionTables1742488572894 implements MigrationInterface {
name = 'RenamePermissionTables1742488572894';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

View File

@ -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;

View File

@ -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
];

View File

@ -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(

View File

@ -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,
});

View File

@ -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) {}

View File

@ -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) {}

View File

@ -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<TwoFactorMethod[]>;
@Field(() => [SettingsPermissions], { nullable: true })
settingsPermissions?: SettingsPermissions[];
@Field(() => [SettingPermissionType], { nullable: true })
settingsPermissions?: SettingPermissionType[];
@Field(() => [PermissionsOnAllObjectRecords], { nullable: true })
objectRecordsPermissions?: PermissionsOnAllObjectRecords[];

View File

@ -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 = (

View File

@ -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()

View File

@ -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<Workspace> {
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<Workspace> {
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId,
workspaceId,
_setting: SettingsPermissions.WORKSPACE,
_setting: SettingPermissionType.WORKSPACE,
isExecutedByApiKey: isDefined(apiKey),
});

View File

@ -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);

View File

@ -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<CanActivate> => {
@Injectable()
class SettingsPermissionsMixin implements CanActivate {

View File

@ -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,

View File

@ -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<ObjectPermissionsEntity[]>;
objectPermissions: Relation<ObjectPermissionEntity[]>;
}

View File

@ -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 },

View File

@ -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,

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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 {}

View File

@ -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<ObjectPermissionEntity>,
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
public async upsertObjectPermission({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertObjectPermissionInput;
}): Promise<ObjectPermissionEntity | null> {
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<RoleEntity | null> {
return this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
});
}
}

View File

@ -1,4 +1,4 @@
export enum SettingsPermissions {
export enum SettingPermissionType {
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
WORKSPACE = 'WORKSPACE',
WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS',

View File

@ -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',
}

View File

@ -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, boolean>;
settingsPermissions: Record<SettingPermissionType, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
}> {
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<SettingsPermissions, boolean>,
{} as Record<SettingPermissionType, boolean>,
);
const objectRecordsPermissionsMap: Record<
@ -83,7 +83,7 @@ export class PermissionsService {
}: {
userWorkspaceId?: string;
workspaceId: string;
_setting: SettingsPermissions;
_setting: SettingPermissionType;
isExecutedByApiKey: boolean;
}): Promise<boolean> {
if (isExecutedByApiKey) {

View File

@ -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:

View File

@ -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 },

View File

@ -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,

View File

@ -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<UserWorkspaceRoleEntity[]>;
@OneToMany(
() => ObjectPermissionsEntity,
(objectPermissions: ObjectPermissionsEntity) => objectPermissions.role,
() => ObjectPermissionEntity,
(objectPermission: ObjectPermissionEntity) => objectPermission.role,
)
objectPermissions: Relation<ObjectPermissionsEntity[]>;
objectPermissions: Relation<ObjectPermissionEntity[]>;
@OneToMany(
() => SettingsPermissionsEntity,
(settingsPermissions: SettingsPermissionsEntity) =>
settingsPermissions.role,
() => SettingPermissionEntity,
(settingPermission: SettingPermissionEntity) => settingPermission.role,
)
settingsPermissions: Relation<SettingsPermissionsEntity[]>;
settingPermissions: Relation<SettingPermissionEntity[]>;
}

View File

@ -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],

View File

@ -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<RoleDTO> {
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<RoleDTO> {
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,
);
}
}
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
setting: SettingsPermissions;
setting: SettingPermissionType;
@Column({ nullable: true, type: 'boolean' })
canUpdateSetting?: boolean;

View File

@ -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 {}

View File

@ -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<SettingPermissionEntity>,
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
) {}
public async upsertSettingPermission({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertSettingPermissionInput;
}): Promise<SettingPermissionEntity | null | undefined> {
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;
}
}
}

View File

@ -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),
})
) {

View File

@ -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,
);
});
});
});
});
});