[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
@@ -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'];
@@ -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}
/> />
} }
> >
@@ -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[] = [];
@@ -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;
}; };
@@ -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(), {
@@ -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`,
@@ -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);
}; };
@@ -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,
); );
@@ -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,
@@ -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 }],
@@ -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"`);
}
}
@@ -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;
@@ -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
]; ];
@@ -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(
@@ -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,
}); });
@@ -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) {}
@@ -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) {}
@@ -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[];
@@ -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 = (
@@ -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()
@@ -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),
}); });
@@ -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);
@@ -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 {
@@ -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,
@@ -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[]>;
} }
@@ -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 },
@@ -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,
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -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 {}
@@ -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,
},
});
}
}
@@ -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',
@@ -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',
} }
@@ -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) {
@@ -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:
@@ -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 },
@@ -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,
@@ -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[]>;
} }
@@ -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],
@@ -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,
);
}
}
} }
@@ -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';
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -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 {}
@@ -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;
}
}
}
@@ -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),
}) })
) { ) {
@@ -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,
);
});
});
});
});
}); });