[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']; uploadImage: Scalars['String'];
uploadProfilePicture: Scalars['String']; uploadProfilePicture: Scalars['String'];
uploadWorkspaceLogo: Scalars['String']; uploadWorkspaceLogo: Scalars['String'];
upsertOneObjectPermission: ObjectPermission;
upsertOneSettingPermission: SettingPermission;
userLookupAdminPanel: UserLookup; userLookupAdminPanel: UserLookup;
validateApprovedAccessDomain: ApprovedAccessDomain; validateApprovedAccessDomain: ApprovedAccessDomain;
}; };
@ -1164,6 +1166,16 @@ export type MutationUploadWorkspaceLogoArgs = {
}; };
export type MutationUpsertOneObjectPermissionArgs = {
upsertObjectPermissionInput: UpsertObjectPermissionInput;
};
export type MutationUpsertOneSettingPermissionArgs = {
upsertSettingPermissionInput: UpsertSettingPermissionInput;
};
export type MutationUserLookupAdminPanelArgs = { export type MutationUserLookupAdminPanelArgs = {
userIdentifier: Scalars['String']; userIdentifier: Scalars['String'];
}; };
@ -1255,6 +1267,17 @@ export type ObjectIndexMetadatasConnection = {
pageInfo: PageInfo; 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 = { export type ObjectRecordFilterInput = {
and?: InputMaybe<Array<ObjectRecordFilterInput>>; and?: InputMaybe<Array<ObjectRecordFilterInput>>;
createdAt?: InputMaybe<DateFilter>; createdAt?: InputMaybe<DateFilter>;
@ -1715,7 +1738,15 @@ export enum ServerlessFunctionSyncStatus {
READY = 'READY' 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', ADMIN_PANEL = 'ADMIN_PANEL',
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
DATA_MODEL = 'DATA_MODEL', DATA_MODEL = 'DATA_MODEL',
@ -1988,6 +2019,21 @@ export type UpdateWorkspaceInput = {
subdomain?: InputMaybe<Scalars['String']>; 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 = { export type User = {
__typename?: 'User'; __typename?: 'User';
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>; analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
@ -2062,7 +2108,7 @@ export type UserWorkspace = {
deletedAt?: Maybe<Scalars['DateTime']>; deletedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['UUID']; id: Scalars['UUID'];
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>; objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
settingsPermissions?: Maybe<Array<SettingsPermissions>>; settingsPermissions?: Maybe<Array<SettingPermissionType>>;
updatedAt: Scalars['DateTime']; updatedAt: Scalars['DateTime'];
user: User; user: User;
userId: Scalars['String']; 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 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; }>; 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 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<{ export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String']; workflowVersionId: Scalars['String'];

View File

@ -5,7 +5,7 @@ import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsPro
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { SettingsPermissions } from '~/generated/graphql'; import { SettingPermissionType } from '~/generated/graphql';
const SettingsApiKeys = lazy(() => const SettingsApiKeys = lazy(() =>
import('~/pages/settings/developers/api-keys/SettingsApiKeys').then( import('~/pages/settings/developers/api-keys/SettingsApiKeys').then(
@ -334,7 +334,7 @@ export const SettingsRoutes = ({
<Route <Route
element={ element={
<SettingsProtectedRouteWrapper <SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.WORKSPACE} settingsPermission={SettingPermissionType.WORKSPACE}
/> />
} }
> >
@ -345,7 +345,7 @@ export const SettingsRoutes = ({
<Route <Route
element={ element={
<SettingsProtectedRouteWrapper <SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.WORKSPACE_MEMBERS} settingsPermission={SettingPermissionType.WORKSPACE_MEMBERS}
/> />
} }
> >
@ -357,7 +357,7 @@ export const SettingsRoutes = ({
<Route <Route
element={ element={
<SettingsProtectedRouteWrapper <SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.DATA_MODEL} settingsPermission={SettingPermissionType.DATA_MODEL}
/> />
} }
> >
@ -387,7 +387,7 @@ export const SettingsRoutes = ({
<Route <Route
element={ element={
<SettingsProtectedRouteWrapper <SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.ROLES} settingsPermission={SettingPermissionType.ROLES}
requiredFeatureFlag={FeatureFlagKey.IsPermissionsEnabled} requiredFeatureFlag={FeatureFlagKey.IsPermissionsEnabled}
/> />
} }
@ -398,7 +398,7 @@ export const SettingsRoutes = ({
<Route <Route
element={ element={
<SettingsProtectedRouteWrapper <SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.API_KEYS_AND_WEBHOOKS} settingsPermission={SettingPermissionType.API_KEYS_AND_WEBHOOKS}
/> />
} }
> >
@ -465,7 +465,7 @@ export const SettingsRoutes = ({
<Route <Route
element={ element={
<SettingsProtectedRouteWrapper <SettingsProtectedRouteWrapper
settingsPermission={SettingsPermissions.SECURITY} settingsPermission={SettingPermissionType.SECURITY}
/> />
} }
> >
@ -496,7 +496,7 @@ export const SettingsRoutes = ({
<Route <Route
element={ element={
<SettingsProtectedRouteWrapper <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 { useCallback, useContext } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; 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 { isDefined } from 'twenty-shared/utils';
import { IconEyeOff, IconSettings } from 'twenty-ui';
import { SettingPermissionType } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
type UseRecordGroupActionsParams = { type UseRecordGroupActionsParams = {
viewType: ViewType; viewType: ViewType;
@ -71,7 +71,7 @@ export const useRecordGroupActions = ({
]); ]);
const hasAccessToDataModelSettings = useHasSettingsPermission( const hasAccessToDataModelSettings = useHasSettingsPermission(
SettingsPermissions.DATA_MODEL, SettingPermissionType.DATA_MODEL,
); );
const recordGroupActions: RecordGroupAction[] = []; const recordGroupActions: RecordGroupAction[] = [];

View File

@ -3,12 +3,12 @@ import { SettingsPath } from '@/types/SettingsPath';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Navigate, Outlet } from 'react-router-dom'; 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'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type SettingsProtectedRouteWrapperProps = { type SettingsProtectedRouteWrapperProps = {
children?: ReactNode; children?: ReactNode;
settingsPermission?: SettingsPermissions; settingsPermission?: SettingPermissionType;
requiredFeatureFlag?: FeatureFlagKey; requiredFeatureFlag?: FeatureFlagKey;
}; };

View File

@ -8,7 +8,7 @@ import {
Billing, Billing,
FeatureFlagKey, FeatureFlagKey,
OnboardingStatus, OnboardingStatus,
SettingsPermissions, SettingPermissionType,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
@ -61,12 +61,12 @@ jest.mock('@/workspace/hooks/useFeatureFlagsMap', () => ({
describe('useSettingsNavigationItems', () => { describe('useSettingsNavigationItems', () => {
it('should hide workspace settings when no permissions', () => { it('should hide workspace settings when no permissions', () => {
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({ (useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
[SettingsPermissions.WORKSPACE]: false, [SettingPermissionType.WORKSPACE]: false,
[SettingsPermissions.WORKSPACE_MEMBERS]: false, [SettingPermissionType.WORKSPACE_MEMBERS]: false,
[SettingsPermissions.DATA_MODEL]: false, [SettingPermissionType.DATA_MODEL]: false,
[SettingsPermissions.API_KEYS_AND_WEBHOOKS]: false, [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: false,
[SettingsPermissions.ROLES]: false, [SettingPermissionType.ROLES]: false,
[SettingsPermissions.SECURITY]: false, [SettingPermissionType.SECURITY]: false,
})); }));
const { result } = renderHook(() => useSettingsNavigationItems(), { const { result } = renderHook(() => useSettingsNavigationItems(), {
@ -82,12 +82,12 @@ describe('useSettingsNavigationItems', () => {
it('should show workspace settings when has permissions', () => { it('should show workspace settings when has permissions', () => {
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({ (useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
[SettingsPermissions.WORKSPACE]: true, [SettingPermissionType.WORKSPACE]: true,
[SettingsPermissions.WORKSPACE_MEMBERS]: true, [SettingPermissionType.WORKSPACE_MEMBERS]: true,
[SettingsPermissions.DATA_MODEL]: true, [SettingPermissionType.DATA_MODEL]: true,
[SettingsPermissions.API_KEYS_AND_WEBHOOKS]: true, [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: true,
[SettingsPermissions.ROLES]: true, [SettingPermissionType.ROLES]: true,
[SettingsPermissions.SECURITY]: true, [SettingPermissionType.SECURITY]: true,
})); }));
const { result } = renderHook(() => useSettingsNavigationItems(), { const { result } = renderHook(() => useSettingsNavigationItems(), {

View File

@ -22,7 +22,6 @@ import {
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { SettingsPermissions } from '~/generated/graphql';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState'; import { billingState } from '@/client-config/states/billingState';
@ -32,6 +31,7 @@ import { NavigationDrawerItemIndentationLevel } from '@/ui/navigation/navigation
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { SettingPermissionType } from '~/generated/graphql';
export type SettingsNavigationSection = { export type SettingsNavigationSection = {
label: string; label: string;
@ -108,13 +108,13 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
label: t`General`, label: t`General`,
path: SettingsPath.Workspace, path: SettingsPath.Workspace,
Icon: IconSettings, Icon: IconSettings,
isHidden: !permissionMap[SettingsPermissions.WORKSPACE], isHidden: !permissionMap[SettingPermissionType.WORKSPACE],
}, },
{ {
label: t`Members`, label: t`Members`,
path: SettingsPath.WorkspaceMembersPage, path: SettingsPath.WorkspaceMembersPage,
Icon: IconUsers, Icon: IconUsers,
isHidden: !permissionMap[SettingsPermissions.WORKSPACE_MEMBERS], isHidden: !permissionMap[SettingPermissionType.WORKSPACE_MEMBERS],
}, },
{ {
label: t`Roles`, label: t`Roles`,
@ -122,33 +122,34 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconLock, Icon: IconLock,
isHidden: isHidden:
!featureFlags[FeatureFlagKey.IsPermissionsEnabled] || !featureFlags[FeatureFlagKey.IsPermissionsEnabled] ||
!permissionMap[SettingsPermissions.ROLES], !permissionMap[SettingPermissionType.ROLES],
}, },
{ {
label: t`Billing`, label: t`Billing`,
path: SettingsPath.Billing, path: SettingsPath.Billing,
Icon: IconCurrencyDollar, Icon: IconCurrencyDollar,
isHidden: isHidden:
!isBillingEnabled || !permissionMap[SettingsPermissions.WORKSPACE], !isBillingEnabled ||
!permissionMap[SettingPermissionType.WORKSPACE],
}, },
{ {
label: t`Data model`, label: t`Data model`,
path: SettingsPath.Objects, path: SettingsPath.Objects,
Icon: IconHierarchy2, Icon: IconHierarchy2,
isHidden: !permissionMap[SettingsPermissions.DATA_MODEL], isHidden: !permissionMap[SettingPermissionType.DATA_MODEL],
}, },
{ {
label: t`Integrations`, label: t`Integrations`,
path: SettingsPath.Integrations, path: SettingsPath.Integrations,
Icon: IconApps, Icon: IconApps,
isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS], isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS],
}, },
{ {
label: t`Security`, label: t`Security`,
path: SettingsPath.Security, path: SettingsPath.Security,
Icon: IconKey, Icon: IconKey,
isAdvanced: true, isAdvanced: true,
isHidden: !permissionMap[SettingsPermissions.SECURITY], isHidden: !permissionMap[SettingPermissionType.SECURITY],
}, },
], ],
}, },
@ -161,14 +162,14 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
path: SettingsPath.APIs, path: SettingsPath.APIs,
Icon: IconApi, Icon: IconApi,
isAdvanced: true, isAdvanced: true,
isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS], isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS],
}, },
{ {
label: t`Webhooks`, label: t`Webhooks`,
path: SettingsPath.Webhooks, path: SettingsPath.Webhooks,
Icon: IconWebhook, Icon: IconWebhook,
isAdvanced: true, isAdvanced: true,
isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS], isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS],
}, },
{ {
label: t`Functions`, label: t`Functions`,
@ -194,7 +195,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
Icon: IconFlask, Icon: IconFlask,
isHidden: isHidden:
!labPublicFeatureFlags.length || !labPublicFeatureFlags.length ||
!permissionMap[SettingsPermissions.WORKSPACE], !permissionMap[SettingPermissionType.WORKSPACE],
}, },
{ {
label: t`Releases`, label: t`Releases`,

View File

@ -1,11 +1,11 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { SettingsPermissions } from '~/generated/graphql';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { SettingPermissionType } from '~/generated/graphql';
export const useHasSettingsPermission = ( export const useHasSettingsPermission = (
settingsPermission?: SettingsPermissions, settingsPermission?: SettingPermissionType,
) => { ) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState); const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
@ -15,19 +15,18 @@ export const useHasSettingsPermission = (
} }
if ( if (
settingsPermission === SettingsPermissions.WORKSPACE && settingsPermission === SettingPermissionType.WORKSPACE &&
currentWorkspace?.activationStatus === currentWorkspace?.activationStatus ===
WorkspaceActivationStatus.PENDING_CREATION WorkspaceActivationStatus.PENDING_CREATION
) { ) {
return true; return true;
} }
const currentUserWorkspaceSettingsPermissions = const currentUserWorkspaceSetting = currentUserWorkspace?.settingsPermissions;
currentUserWorkspace?.settingsPermissions;
if (!currentUserWorkspaceSettingsPermissions) { if (!currentUserWorkspaceSetting) {
return false; 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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { SettingsPermissions } from '~/generated/graphql'; import { SettingPermissionType } from '~/generated/graphql';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue'; import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
export const useSettingsPermissionMap = (): Record< export const useSettingsPermissionMap = (): Record<
SettingsPermissions, SettingPermissionType,
boolean boolean
> => { > => {
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState); const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
@ -19,7 +19,7 @@ export const useSettingsPermissionMap = (): Record<
currentUserWorkspace?.settingsPermissions; currentUserWorkspace?.settingsPermissions;
const initialPermissions = buildRecordFromKeysWithSameValue( const initialPermissions = buildRecordFromKeysWithSameValue(
Object.values(SettingsPermissions), Object.values(SettingPermissionType),
!isPermissionEnabled, !isPermissionEnabled,
); );

View File

@ -21,7 +21,7 @@ import {
Section, Section,
} from 'twenty-ui'; } from 'twenty-ui';
import { Role } from '~/generated-metadata/graphql'; import { Role } from '~/generated-metadata/graphql';
import { SettingsPermissions } from '~/generated/graphql'; import { SettingPermissionType } from '~/generated/graphql';
import { RolePermissionsObjectsTableRow } from './RolePermissionsObjectsTableRow'; import { RolePermissionsObjectsTableRow } from './RolePermissionsObjectsTableRow';
const StyledRolePermissionsContainer = styled.div` const StyledRolePermissionsContainer = styled.div`
@ -81,49 +81,49 @@ export const RolePermissions = ({ role }: RolePermissionsProps) => {
const settingsPermissionsConfig: RolePermissionsSettingPermission[] = [ const settingsPermissionsConfig: RolePermissionsSettingPermission[] = [
{ {
key: SettingsPermissions.API_KEYS_AND_WEBHOOKS, key: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
name: 'API Keys & Webhooks', name: 'API Keys & Webhooks',
description: 'Manage API keys and webhooks', description: 'Manage API keys and webhooks',
value: role.canUpdateAllSettings, value: role.canUpdateAllSettings,
Icon: IconCode, Icon: IconCode,
}, },
{ {
key: SettingsPermissions.WORKSPACE, key: SettingPermissionType.WORKSPACE,
name: 'Workspace', name: 'Workspace',
description: 'Set global workspace preferences', description: 'Set global workspace preferences',
value: role.canUpdateAllSettings, value: role.canUpdateAllSettings,
Icon: IconSettings, Icon: IconSettings,
}, },
{ {
key: SettingsPermissions.WORKSPACE_MEMBERS, key: SettingPermissionType.WORKSPACE_MEMBERS,
name: 'Users', name: 'Users',
description: 'Add or remove users', description: 'Add or remove users',
value: role.canUpdateAllSettings, value: role.canUpdateAllSettings,
Icon: IconUsers, Icon: IconUsers,
}, },
{ {
key: SettingsPermissions.ROLES, key: SettingPermissionType.ROLES,
name: 'Roles', name: 'Roles',
description: 'Define user roles and access levels', description: 'Define user roles and access levels',
value: role.canUpdateAllSettings, value: role.canUpdateAllSettings,
Icon: IconLockOpen, Icon: IconLockOpen,
}, },
{ {
key: SettingsPermissions.DATA_MODEL, key: SettingPermissionType.DATA_MODEL,
name: 'Data Model', name: 'Data Model',
description: 'Edit CRM data structure and fields', description: 'Edit CRM data structure and fields',
value: role.canUpdateAllSettings, value: role.canUpdateAllSettings,
Icon: IconHierarchy, Icon: IconHierarchy,
}, },
{ {
key: SettingsPermissions.ADMIN_PANEL, key: SettingPermissionType.ADMIN_PANEL,
name: 'Admin Panel', name: 'Admin Panel',
description: 'Admin settings and system tools', description: 'Admin settings and system tools',
value: role.canUpdateAllSettings, value: role.canUpdateAllSettings,
Icon: IconServer, Icon: IconServer,
}, },
{ {
key: SettingsPermissions.SECURITY, key: SettingPermissionType.SECURITY,
name: 'Security', name: 'Security',
description: 'Manage security policies', description: 'Manage security policies',
value: role.canUpdateAllSettings, value: role.canUpdateAllSettings,

View File

@ -3,7 +3,7 @@ import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { import {
FeatureFlagKey, FeatureFlagKey,
OnboardingStatus, OnboardingStatus,
SettingsPermissions, SettingPermissionType,
SubscriptionInterval, SubscriptionInterval,
SubscriptionStatus, SubscriptionStatus,
User, User,
@ -131,7 +131,7 @@ export const mockedUserData: MockedUser = {
workspaceMember: mockedWorkspaceMemberData, workspaceMember: mockedWorkspaceMemberData,
currentWorkspace: mockCurrentWorkspace, currentWorkspace: mockCurrentWorkspace,
currentUserWorkspace: { currentUserWorkspace: {
settingsPermissions: [SettingsPermissions.WORKSPACE_MEMBERS], settingsPermissions: [SettingPermissionType.WORKSPACE_MEMBERS],
}, },
locale: 'en', locale: 'en',
workspaces: [{ workspace: mockCurrentWorkspace }], 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 = { export const SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS = {
apiKey: SettingsPermissions.API_KEYS_AND_WEBHOOKS, apiKey: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
webhook: SettingsPermissions.API_KEYS_AND_WEBHOOKS, webhook: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
} as const; } as const;

View File

@ -1,9 +1,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields'; import graphqlFields from 'graphql-fields';
import { DataSource, ObjectLiteral } from 'typeorm';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; 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 { 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'; 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 { 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 { 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 { 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 { import {
PermissionsException, PermissionsException,
PermissionsExceptionCode, PermissionsExceptionCode,
@ -190,7 +190,7 @@ export abstract class GraphqlQueryBaseResolverService<
objectMetadataItemWithFieldMaps.nameSingular, objectMetadataItemWithFieldMaps.nameSingular,
) )
) { ) {
const permissionRequired: SettingsPermissions = const permissionRequired: SettingPermissionType =
SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS[ SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS[
objectMetadataItemWithFieldMaps.nameSingular objectMetadataItemWithFieldMaps.nameSingular
]; ];

View File

@ -3,8 +3,8 @@ import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import omit from 'lodash.omit'; import omit from 'lodash.omit';
import { Repository } from 'typeorm';
import { SOURCE_LOCALE } from 'twenty-shared/translations'; 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 { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-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 { 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 { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service'; 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 { 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 { 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'; 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 { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { 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 { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input'; import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
@ -367,7 +367,7 @@ export class AuthResolver {
@UseGuards( @UseGuards(
WorkspaceAuthGuard, WorkspaceAuthGuard,
SettingsPermissionsGuard(SettingsPermissions.API_KEYS_AND_WEBHOOKS), SettingsPermissionsGuard(SettingPermissionType.API_KEYS_AND_WEBHOOKS),
) )
@Mutation(() => ApiKeyToken) @Mutation(() => ApiKeyToken)
async generateApiKeyToken( 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 { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { import {
PermissionsException, PermissionsException,
PermissionsExceptionCode, PermissionsExceptionCode,
@ -52,7 +52,7 @@ export class BillingResolver {
@Query(() => BillingSessionOutput) @Query(() => BillingSessionOutput)
@UseGuards( @UseGuards(
WorkspaceAuthGuard, WorkspaceAuthGuard,
SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), SettingsPermissionsGuard(SettingPermissionType.WORKSPACE),
) )
async billingPortalSession( async billingPortalSession(
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@ -115,7 +115,7 @@ export class BillingResolver {
@Mutation(() => BillingUpdateOutput) @Mutation(() => BillingUpdateOutput)
@UseGuards( @UseGuards(
WorkspaceAuthGuard, WorkspaceAuthGuard,
SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), SettingsPermissionsGuard(SettingPermissionType.WORKSPACE),
) )
async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) { async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) {
await this.billingSubscriptionService.applyBillingSubscription(workspace); await this.billingSubscriptionService.applyBillingSubscription(workspace);
@ -161,7 +161,7 @@ export class BillingResolver {
await this.permissionsService.userHasWorkspaceSettingPermission({ await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId, userWorkspaceId,
workspaceId, workspaceId,
_setting: SettingsPermissions.WORKSPACE, _setting: SettingPermissionType.WORKSPACE,
isExecutedByApiKey, 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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@Resolver() @Resolver()
@UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter) @UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter)
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.WORKSPACE)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.WORKSPACE))
export class LabResolver { export class LabResolver {
constructor(private featureFlagService: FeatureFlagService) {} 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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@Resolver() @Resolver()
@UseFilters(PermissionsGraphqlApiExceptionFilter) @UseFilters(PermissionsGraphqlApiExceptionFilter)
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.SECURITY)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.SECURITY))
export class SSOResolver { export class SSOResolver {
constructor(private readonly sSOService: SSOService) {} constructor(private readonly sSOService: SSOService) {}

View File

@ -1,6 +1,7 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { import {
Column, Column,
CreateDateColumn, CreateDateColumn,
@ -14,16 +15,15 @@ import {
Unique, Unique,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; 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 { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.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, { registerEnumType(SettingPermissionType, {
name: 'SettingsPermissions', name: 'SettingPermissionType',
}); });
registerEnumType(PermissionsOnAllObjectRecords, { registerEnumType(PermissionsOnAllObjectRecords, {
@ -78,8 +78,8 @@ export class UserWorkspace {
) )
twoFactorMethods: Relation<TwoFactorMethod[]>; twoFactorMethods: Relation<TwoFactorMethod[]>;
@Field(() => [SettingsPermissions], { nullable: true }) @Field(() => [SettingPermissionType], { nullable: true })
settingsPermissions?: SettingsPermissions[]; settingsPermissions?: SettingPermissionType[];
@Field(() => [PermissionsOnAllObjectRecords], { nullable: true }) @Field(() => [PermissionsOnAllObjectRecords], { nullable: true })
objectRecordsPermissions?: PermissionsOnAllObjectRecords[]; objectRecordsPermissions?: PermissionsOnAllObjectRecords[];

View File

@ -13,8 +13,8 @@ import crypto from 'crypto';
import { GraphQLJSONObject } from 'graphql-type-json'; import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload'; import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { In, Repository } from 'typeorm';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { In, Repository } from 'typeorm';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator'; import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; 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 { 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 { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
@ -122,8 +122,8 @@ export class UserResolver {
workspaceId: workspace.id, workspaceId: workspace.id,
}); });
const grantedSettingsPermissions: SettingsPermissions[] = ( const grantedSettingsPermissions: SettingPermissionType[] = (
Object.keys(settingsPermissions) as SettingsPermissions[] Object.keys(settingsPermissions) as SettingPermissionType[]
).filter((feature) => settingsPermissions[feature] === true); ).filter((feature) => settingsPermissions[feature] === true);
const grantedObjectRecordsPermissions = ( 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 { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { SendInvitationsInput } from './dtos/send-invitations.input'; import { SendInvitationsInput } from './dtos/send-invitations.input';
@UseGuards( @UseGuards(
WorkspaceAuthGuard, WorkspaceAuthGuard,
SettingsPermissionsGuard(SettingsPermissions.WORKSPACE_MEMBERS), SettingsPermissionsGuard(SettingPermissionType.WORKSPACE_MEMBERS),
) )
@UseFilters(PermissionsGraphqlApiExceptionFilter) @UseFilters(PermissionsGraphqlApiExceptionFilter)
@Resolver() @Resolver()

View File

@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import assert from 'assert'; import assert from 'assert';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; 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'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
@ -34,7 +34,7 @@ import {
WorkspaceExceptionCode, WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception'; } from 'src/engine/core-modules/workspace/workspace.exception';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; 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 { import {
PermissionsException, PermissionsException,
PermissionsExceptionCode, PermissionsExceptionCode,
@ -442,7 +442,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
const userHasPermission = const userHasPermission =
await this.permissionsService.userHasWorkspaceSettingPermission({ await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId, userWorkspaceId,
_setting: SettingsPermissions.SECURITY, _setting: SettingPermissionType.SECURITY,
workspaceId: workspaceId, workspaceId: workspaceId,
isExecutedByApiKey: isDefined(apiKey), isExecutedByApiKey: isDefined(apiKey),
}); });
@ -481,7 +481,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
await this.permissionsService.userHasWorkspaceSettingPermission({ await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId, userWorkspaceId,
workspaceId, workspaceId,
_setting: SettingsPermissions.WORKSPACE, _setting: SettingPermissionType.WORKSPACE,
isExecutedByApiKey: isDefined(apiKey), isExecutedByApiKey: isDefined(apiKey),
}); });

View File

@ -12,8 +12,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import assert from 'assert'; import assert from 'assert';
import { FileUpload, GraphQLUpload } from 'graphql-upload'; import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; 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 { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { 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 { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { RoleService } from 'src/engine/metadata-modules/role/role.service';
@ -130,7 +130,7 @@ export class WorkspaceResolver {
@Mutation(() => String) @Mutation(() => String)
@UseGuards( @UseGuards(
WorkspaceAuthGuard, WorkspaceAuthGuard,
SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), SettingsPermissionsGuard(SettingPermissionType.WORKSPACE),
) )
async uploadWorkspaceLogo( async uploadWorkspaceLogo(
@AuthWorkspace() { id }: Workspace, @AuthWorkspace() { id }: Workspace,
@ -174,7 +174,7 @@ export class WorkspaceResolver {
@Mutation(() => Workspace) @Mutation(() => Workspace)
@UseGuards( @UseGuards(
WorkspaceAuthGuard, WorkspaceAuthGuard,
SettingsPermissionsGuard(SettingsPermissions.WORKSPACE), SettingsPermissionsGuard(SettingPermissionType.WORKSPACE),
) )
async deleteCurrentWorkspace(@AuthWorkspace() { id }: Workspace) { async deleteCurrentWorkspace(@AuthWorkspace() { id }: Workspace) {
return this.workspaceService.deleteWorkspace(id); 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 { 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 { 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 { import {
PermissionsException, PermissionsException,
PermissionsExceptionCode, PermissionsExceptionCode,
@ -20,7 +20,7 @@ import {
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
export const SettingsPermissionsGuard = ( export const SettingsPermissionsGuard = (
requiredPermission: SettingsPermissions, requiredPermission: SettingPermissionType,
): Type<CanActivate> => { ): Type<CanActivate> => {
@Injectable() @Injectable()
class SettingsPermissionsMixin implements CanActivate { class SettingsPermissionsMixin implements CanActivate {

View File

@ -36,7 +36,7 @@ import {
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; 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 { 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 { 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 { 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'; 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 }) @ResolveField(() => String, { nullable: true })
async icon( async icon(
@Parent() fieldMetadata: FieldMetadataDTO, @Parent() fieldMetadata: FieldMetadataDTO,
@ -86,7 +87,7 @@ export class FieldMetadataResolver {
); );
} }
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@Mutation(() => FieldMetadataDTO) @Mutation(() => FieldMetadataDTO)
async createOneField( async createOneField(
@Args('input') input: CreateOneFieldMetadataInput, @Args('input') input: CreateOneFieldMetadataInput,
@ -102,7 +103,7 @@ export class FieldMetadataResolver {
} }
} }
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@Mutation(() => FieldMetadataDTO) @Mutation(() => FieldMetadataDTO)
async updateOneField( async updateOneField(
@Args('input') input: UpdateOneFieldMetadataInput, @Args('input') input: UpdateOneFieldMetadataInput,
@ -123,7 +124,7 @@ export class FieldMetadataResolver {
} }
} }
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@Mutation(() => FieldMetadataDTO) @Mutation(() => FieldMetadataDTO)
async deleteOneField( async deleteOneField(
@Args('input') input: DeleteOneFieldInput, @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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-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 { 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'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@Entity('objectMetadata') @Entity('objectMetadata')
@ -142,9 +142,12 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
updatedAt: Date; updatedAt: Date;
@OneToMany( @OneToMany(
() => ObjectPermissionsEntity, () => ObjectPermissionEntity,
(objectPermissions: ObjectPermissionsEntity) => (objectPermission: ObjectPermissionEntity) =>
objectPermissions.objectMetadata, 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 { 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 { 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 { 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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; 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'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@ -78,7 +78,9 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
}, },
create: { create: {
many: { disabled: true }, many: { disabled: true },
guards: [SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)], guards: [
SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL),
],
}, },
update: { disabled: true }, update: { disabled: true },
delete: { 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 { 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 { 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 { 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'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@UseGuards(WorkspaceAuthGuard) @UseGuards(WorkspaceAuthGuard)
@ -72,6 +72,7 @@ export class ObjectMetadataResolver {
); );
} }
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@ResolveField(() => String, { nullable: true }) @ResolveField(() => String, { nullable: true })
async icon( async icon(
@Parent() objectMetadata: ObjectMetadataDTO, @Parent() objectMetadata: ObjectMetadataDTO,
@ -84,7 +85,7 @@ export class ObjectMetadataResolver {
); );
} }
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@Mutation(() => ObjectMetadataDTO) @Mutation(() => ObjectMetadataDTO)
async deleteOneObject( async deleteOneObject(
@Args('input') input: DeleteOneObjectInput, @Args('input') input: DeleteOneObjectInput,
@ -100,7 +101,7 @@ export class ObjectMetadataResolver {
} }
} }
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@Mutation(() => ObjectMetadataDTO) @Mutation(() => ObjectMetadataDTO)
async updateOneObject( async updateOneObject(
@Args('input') input: UpdateOneObjectInput, @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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@Entity('objectPermissions') @Entity('objectPermission')
@Unique('IndexOnObjectPermissionsUnique', ['objectMetadataId', 'roleId']) @Unique('IndexOnObjectPermissionUnique', ['objectMetadataId', 'roleId'])
export class ObjectPermissionsEntity { export class ObjectPermissionEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; 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', API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
WORKSPACE = 'WORKSPACE', WORKSPACE = 'WORKSPACE',
WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS', WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS',

View File

@ -25,6 +25,9 @@ export enum PermissionsExceptionCode {
PERMISSIONS_V2_NOT_ENABLED = 'PERMISSIONS_V2_NOT_ENABLED', PERMISSIONS_V2_NOT_ENABLED = 'PERMISSIONS_V2_NOT_ENABLED',
ROLE_LABEL_ALREADY_EXISTS = 'ROLE_LABEL_ALREADY_EXISTS', ROLE_LABEL_ALREADY_EXISTS = 'ROLE_LABEL_ALREADY_EXISTS',
DEFAULT_ROLE_NOT_FOUND = 'DEFAULT_ROLE_NOT_FOUND', 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 { export enum PermissionsExceptionMessage {
@ -45,4 +48,7 @@ export enum PermissionsExceptionMessage {
PERMISSIONS_V2_NOT_ENABLED = 'Permissions V2 is not enabled', PERMISSIONS_V2_NOT_ENABLED = 'Permissions V2 is not enabled',
ROLE_LABEL_ALREADY_EXISTS = 'A role with this label already exists', ROLE_LABEL_ALREADY_EXISTS = 'A role with this label already exists',
DEFAULT_ROLE_NOT_FOUND = 'Default role not found', 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 { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { import {
AuthException, AuthException,
AuthExceptionCode, AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception'; } from 'src/engine/core-modules/auth/auth.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; 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 { import {
PermissionsException, PermissionsException,
PermissionsExceptionCode, PermissionsExceptionCode,
@ -31,7 +31,7 @@ export class PermissionsService {
userWorkspaceId: string; userWorkspaceId: string;
workspaceId: string; workspaceId: string;
}): Promise<{ }): Promise<{
settingsPermissions: Record<SettingsPermissions, boolean>; settingsPermissions: Record<SettingPermissionType, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>; objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
}> { }> {
const [roleOfUserWorkspace] = await this.userRoleService const [roleOfUserWorkspace] = await this.userRoleService
@ -47,12 +47,12 @@ export class PermissionsService {
hasPermissionOnSettingFeature = true; hasPermissionOnSettingFeature = true;
} }
const settingsPermissionsMap = Object.keys(SettingsPermissions).reduce( const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce(
(acc, feature) => ({ (acc, feature) => ({
...acc, ...acc,
[feature]: hasPermissionOnSettingFeature, [feature]: hasPermissionOnSettingFeature,
}), }),
{} as Record<SettingsPermissions, boolean>, {} as Record<SettingPermissionType, boolean>,
); );
const objectRecordsPermissionsMap: Record< const objectRecordsPermissionsMap: Record<
@ -83,7 +83,7 @@ export class PermissionsService {
}: { }: {
userWorkspaceId?: string; userWorkspaceId?: string;
workspaceId: string; workspaceId: string;
_setting: SettingsPermissions; _setting: SettingPermissionType;
isExecutedByApiKey: boolean; isExecutedByApiKey: boolean;
}): Promise<boolean> { }): Promise<boolean> {
if (isExecutedByApiKey) { if (isExecutedByApiKey) {

View File

@ -19,11 +19,14 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER: case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER:
case PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED: case PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED:
case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS: case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS:
case PermissionsExceptionCode.ROLE_NOT_EDITABLE:
throw new ForbiddenError(error.message); throw new ForbiddenError(error.message);
case PermissionsExceptionCode.INVALID_ARG: case PermissionsExceptionCode.INVALID_ARG:
case PermissionsExceptionCode.INVALID_SETTING:
throw new UserInputError(error.message); throw new UserInputError(error.message);
case PermissionsExceptionCode.ROLE_NOT_FOUND: case PermissionsExceptionCode.ROLE_NOT_FOUND:
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND: case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
throw new NotFoundError(error.message); throw new NotFoundError(error.message);
case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND: case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND:
default: 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 { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-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 { 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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; 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'; 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, pagingStrategy: PagingStrategies.CURSOR,
create: { create: {
many: { disabled: true }, many: { disabled: true },
guards: [SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)], guards: [
SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL),
],
}, },
update: { disabled: true }, update: { disabled: true },
delete: { 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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-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 { 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 { 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'; import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
@ -20,7 +20,7 @@ export class RelationMetadataResolver {
private readonly relationMetadataService: RelationMetadataService, private readonly relationMetadataService: RelationMetadataService,
) {} ) {}
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@Mutation(() => RelationMetadataDTO) @Mutation(() => RelationMetadataDTO)
async deleteOneRelation( async deleteOneRelation(
@Args('input') input: DeleteOneRelationInput, @Args('input') input: DeleteOneRelationInput,

View File

@ -9,9 +9,9 @@ import {
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } 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 { 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') @Entity('role')
@Unique('IndexOnRoleUnique', ['label', 'workspaceId']) @Unique('IndexOnRoleUnique', ['label', 'workspaceId'])
@ -62,15 +62,14 @@ export class RoleEntity {
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>; userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
@OneToMany( @OneToMany(
() => ObjectPermissionsEntity, () => ObjectPermissionEntity,
(objectPermissions: ObjectPermissionsEntity) => objectPermissions.role, (objectPermission: ObjectPermissionEntity) => objectPermission.role,
) )
objectPermissions: Relation<ObjectPermissionsEntity[]>; objectPermissions: Relation<ObjectPermissionEntity[]>;
@OneToMany( @OneToMany(
() => SettingsPermissionsEntity, () => SettingPermissionEntity,
(settingsPermissions: SettingsPermissionsEntity) => (settingPermission: SettingPermissionEntity) => settingPermission.role,
settingsPermissions.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 { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; 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 { 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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver'; import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver';
import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; 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'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
@Module({ @Module({
@ -19,6 +21,8 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.
PermissionsModule, PermissionsModule,
UserWorkspaceModule, UserWorkspaceModule,
FeatureFlagModule, FeatureFlagModule,
ObjectPermissionModule,
SettingPermissionModule,
], ],
providers: [RoleService, RoleResolver], providers: [RoleService, RoleResolver],
exports: [RoleService], 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 { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; 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 { import {
PermissionsException, PermissionsException,
PermissionsExceptionCode, PermissionsExceptionCode,
PermissionsExceptionMessage, PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception'; } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; 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 { 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 { 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 { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Resolver(() => RoleDTO) @Resolver(() => RoleDTO)
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.ROLES)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.ROLES))
@UseFilters(PermissionsGraphqlApiExceptionFilter) @UseFilters(PermissionsGraphqlApiExceptionFilter)
export class RoleResolver { export class RoleResolver {
constructor( constructor(
@ -39,6 +45,8 @@ export class RoleResolver {
private readonly roleService: RoleService, private readonly roleService: RoleService,
private readonly userWorkspaceService: UserWorkspaceService, private readonly userWorkspaceService: UserWorkspaceService,
private readonly featureFlagService: FeatureFlagService, private readonly featureFlagService: FeatureFlagService,
private readonly objectPermissionService: ObjectPermissionService,
private readonly settingPermissionService: SettingPermissionService,
) {} ) {}
@Query(() => [RoleDTO]) @Query(() => [RoleDTO])
@ -101,18 +109,7 @@ export class RoleResolver {
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@Args('createRoleInput') createRoleInput: CreateRoleInput, @Args('createRoleInput') createRoleInput: CreateRoleInput,
): Promise<RoleDTO> { ): Promise<RoleDTO> {
const isPermissionsV2Enabled = await this.validatePermissionsV2EnabledOrThrow(workspace);
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
workspace.id,
);
if (!isPermissionsV2Enabled) {
throw new PermissionsException(
PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED,
PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED,
);
}
return this.roleService.createRole({ return this.roleService.createRole({
workspaceId: workspace.id, workspaceId: workspace.id,
@ -125,18 +122,11 @@ export class RoleResolver {
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@Args('updateRoleInput') updateRoleInput: UpdateRoleInput, @Args('updateRoleInput') updateRoleInput: UpdateRoleInput,
): Promise<RoleDTO> { ): Promise<RoleDTO> {
const isPermissionsV2Enabled = await this.validatePermissionsV2EnabledOrThrow(workspace);
await this.featureFlagService.isFeatureEnabled( await this.validateRoleIsEditableOrThrow({
FeatureFlagKey.IsPermissionsV2Enabled, roleId: updateRoleInput.id,
workspace.id, workspaceId: workspace.id,
); });
if (!isPermissionsV2Enabled) {
throw new PermissionsException(
PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED,
PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED,
);
}
return this.roleService.updateRole({ return this.roleService.updateRole({
input: updateRoleInput, 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]) @ResolveField('workspaceMembers', () => [WorkspaceMember])
async getWorkspaceMembersAssignedToRole( async getWorkspaceMembersAssignedToRole(
@Parent() role: RoleDTO, @Parent() role: RoleDTO,
@ -154,4 +180,36 @@ export class RoleResolver {
workspace.id, 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 { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils'; 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 { 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'; import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants';
@ -10,11 +10,11 @@ import {
PermissionsExceptionCode, PermissionsExceptionCode,
PermissionsExceptionMessage, PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception'; } 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 { import {
UpdateRoleInput, UpdateRoleInput,
UpdateRolePayload, 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 { 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'; 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, UpdateDateColumn,
} from 'typeorm'; } 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'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@Entity('settingsPermissions') @Entity('settingPermission')
@Unique('IndexOnSettingsPermissionsUnique', ['setting', 'roleId']) @Unique('IndexOnSettingPermissionUnique', ['setting', 'roleId'])
export class SettingsPermissionsEntity { export class SettingPermissionEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({ nullable: false, type: 'uuid' }) @Column({ nullable: false, type: 'uuid' })
roleId: string; roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.settingsPermissions, { @ManyToOne(() => RoleEntity, (role) => role.settingPermissions, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}) })
@JoinColumn({ name: 'roleId' }) @JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>; role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'varchar' }) @Column({ nullable: false, type: 'varchar' })
setting: SettingsPermissions; setting: SettingPermissionType;
@Column({ nullable: true, type: 'boolean' }) @Column({ nullable: true, type: 'boolean' })
canUpdateSetting?: 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 { 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 { 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 { import {
PermissionsException, PermissionsException,
PermissionsExceptionCode, PermissionsExceptionCode,
@ -65,7 +65,7 @@ export class WorkspaceMemberPreQueryHookService {
await this.permissionsService.userHasWorkspaceSettingPermission({ await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId, userWorkspaceId,
workspaceId, workspaceId,
_setting: SettingsPermissions.WORKSPACE_MEMBERS, _setting: SettingPermissionType.WORKSPACE_MEMBERS,
isExecutedByApiKey: isDefined(apiKey), isExecutedByApiKey: isDefined(apiKey),
}) })
) { ) {

View File

@ -1,15 +1,41 @@
import request from 'supertest'; import request from 'supertest';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; 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 { 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 { 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 { 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 { 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'; import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
const client = request(`http://localhost:${APP_PORT}`); 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', () => { describe('roles permissions', () => {
let adminRoleId: string;
let guestRoleId: string;
beforeAll(async () => { beforeAll(async () => {
const enablePermissionsQuery = updateFeatureFlagFactory( const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID, SEED_APPLE_WORKSPACE_ID,
@ -17,7 +43,38 @@ describe('roles permissions', () => {
true, true,
); );
const enablePermissionsV2Query = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
true,
);
await makeGraphqlAPIRequest(enablePermissionsQuery); 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 () => { afterAll(async () => {
@ -27,7 +84,14 @@ describe('roles permissions', () => {
false, false,
); );
const disablePermissionsV2Query = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
false,
);
await makeGraphqlAPIRequest(disablePermissionsQuery); await makeGraphqlAPIRequest(disablePermissionsQuery);
await makeGraphqlAPIRequest(disablePermissionsV2Query);
}); });
describe('getRoles', () => { describe('getRoles', () => {
@ -116,19 +180,7 @@ describe('roles permissions', () => {
`, `,
}; };
await client await assertPermissionDeniedForMemberWithMemberRole({ query });
.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);
});
}); });
}); });
@ -144,19 +196,7 @@ describe('roles permissions', () => {
`, `,
}; };
await client await assertPermissionDeniedForMemberWithMemberRole({ query });
.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);
});
}); });
it('should throw a permission error when tries to update their own role (admin role)', async () => { 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,
);
});
});
});
});
}); });