Rollout field permissions (#14024)

Rolling out field permissions feature
This commit is contained in:
Weiko
2025-08-21 16:17:28 +02:00
committed by GitHub
parent fa78762cbb
commit 707043c3ba
38 changed files with 339 additions and 645 deletions
@@ -955,7 +955,6 @@ export enum FeatureFlagKey {
IS_API_KEY_ROLES_ENABLED = 'IS_API_KEY_ROLES_ENABLED',
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
@@ -919,7 +919,6 @@ export enum FeatureFlagKey {
IS_API_KEY_ROLES_ENABLED = 'IS_API_KEY_ROLES_ENABLED',
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
@@ -6,9 +6,9 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
import { type ObjectPermissions } from 'twenty-shared/types';
type MapFieldMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[];
@@ -23,7 +23,6 @@ type MapFieldMetadataToGraphQLQueryArgs = {
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
// TODO: change ObjectMetadataItems mock before refactoring with relation computed field
export const mapFieldMetadataToGraphQLQuery = ({
@@ -33,7 +32,6 @@ export const mapFieldMetadataToGraphQLQuery = ({
relationRecordGqlFields,
computeReferences = false,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
}: MapFieldMetadataToGraphQLQueryArgs): string => {
const fieldType = fieldMetadata.type;
@@ -89,7 +87,6 @@ ${mapObjectMetadataToGraphQLQuery({
computeReferences: computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}`;
}
@@ -137,7 +134,6 @@ ${mapObjectMetadataToGraphQLQuery({
computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
}
}`;
@@ -20,7 +20,6 @@ type MapObjectMetadataToGraphQLQueryArgs = {
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
export const mapObjectMetadataToGraphQLQuery = ({
@@ -30,7 +29,6 @@ export const mapObjectMetadataToGraphQLQuery = ({
computeReferences = false,
isRootLevel = true,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled = false,
}: MapObjectMetadataToGraphQLQueryArgs): string => {
if (
!isRootLevel &&
@@ -103,7 +101,6 @@ export const mapObjectMetadataToGraphQLQuery = ({
relationRecordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
});
})
.filter((field) => field !== '')
@@ -11,9 +11,7 @@ import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphq
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { prefillRecord } from '@/object-record/utils/prefillRecord';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const useCreateOneRecordInCache = <T extends ObjectRecord>({
objectMetadataItem,
@@ -27,9 +25,6 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloCoreClient = useApolloCoreClient();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
return (record: ObjectRecord) => {
const prefilledRecord = prefillRecord({
@@ -51,7 +46,6 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
computeReferences: true,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
`;
@@ -7,9 +7,7 @@ import { type RecordGqlOperationVariables } from '@/object-record/graphql/types/
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const useReadFindManyRecordsQueryInCache = ({
objectMetadataItem,
@@ -22,10 +20,6 @@ export const useReadFindManyRecordsQueryInCache = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const readFindManyRecordsQueryInCache = <
T extends ObjectRecord = ObjectRecord,
>({
@@ -40,7 +34,6 @@ export const useReadFindManyRecordsQueryInCache = ({
objectMetadataItems,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
});
const existingRecordsQueryResult =
@@ -8,8 +8,6 @@ import { type RecordGqlOperationVariables } from '@/object-record/graphql/types/
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { FeatureFlagKey } from '~/generated/graphql';
export const useUpsertFindManyRecordsQueryInCache = ({
objectMetadataItem,
@@ -21,10 +19,6 @@ export const useUpsertFindManyRecordsQueryInCache = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const upsertFindManyRecordsQueryInCache = <
T extends ObjectRecord = ObjectRecord,
>({
@@ -44,7 +38,6 @@ export const useUpsertFindManyRecordsQueryInCache = ({
recordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
});
const newObjectRecordConnection = getRecordConnectionFromRecords({
@@ -6,10 +6,10 @@ import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFr
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { type ObjectPermissions } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { isEmptyObject } from '~/utils/isEmptyObject';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { type ObjectPermissions } from 'twenty-shared/types';
export type GetRecordFromCacheArgs = {
cache: ApolloCache<object>;
@@ -21,7 +21,6 @@ export type GetRecordFromCacheArgs = {
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItem,
@@ -30,7 +29,6 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
recordId,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled = false,
}: GetRecordFromCacheArgs) => {
if (isUndefinedOrNull(objectMetadataItem)) {
return null;
@@ -48,7 +46,6 @@ export const getRecordFromCache = <T extends ObjectRecord = ObjectRecord>({
objectMetadataItem,
recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
},
)}
`;
@@ -17,7 +17,6 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
recordGqlFields,
record,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
}: {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: ObjectMetadataItem;
@@ -28,7 +27,6 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
}) => {
if (isUndefinedOrNull(objectMetadataItem)) {
return null;
@@ -44,7 +42,6 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
computeReferences: true,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
},
)}
`;
@@ -8,9 +8,7 @@ import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateManyRecordsMutation = ({
@@ -28,10 +26,6 @@ export const useCreateManyRecordsMutation = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
if (isUndefinedOrNull(objectMetadataItem)) {
return { createManyRecordsMutation: EMPTY_MUTATION };
}
@@ -52,7 +46,6 @@ export const useCreateManyRecordsMutation = ({
objectMetadataItem,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
},
)}
}`;
@@ -9,9 +9,7 @@ import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateOneRecordMutation = ({
@@ -35,10 +33,6 @@ export const useCreateOneRecordMutation = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
if (isUndefinedOrNull(objectMetadataItem)) {
return { createOneRecordMutation: EMPTY_MUTATION };
}
@@ -56,7 +50,6 @@ export const useCreateOneRecordMutation = ({
objectMetadataItem,
recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
}
`;
@@ -7,9 +7,7 @@ import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabl
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const useFindDuplicateRecordsQuery = ({
objectNameSingular,
@@ -24,10 +22,6 @@ export const useFindDuplicateRecordsQuery = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const findDuplicateRecordsQuery = gql`
query FindDuplicate${capitalize(
objectMetadataItem.nameSingular,
@@ -40,7 +34,6 @@ export const useFindDuplicateRecordsQuery = ({
objectMetadataItems,
objectMetadataItem,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
cursor
}
@@ -8,8 +8,6 @@ import {
generateFindManyRecordsQuery,
type QueryCursorDirection,
} from '@/object-record/utils/generateFindManyRecordsQuery';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { FeatureFlagKey } from '~/generated/graphql';
export const useFindManyRecordsQuery = ({
objectNameSingular,
@@ -30,10 +28,6 @@ export const useFindManyRecordsQuery = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const findManyRecordsQuery = generateFindManyRecordsQuery({
objectMetadataItem,
objectMetadataItems,
@@ -41,7 +35,6 @@ export const useFindManyRecordsQuery = ({
computeReferences,
cursorDirection,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
});
return {
@@ -6,9 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const useFindOneRecordQuery = ({
objectNameSingular,
@@ -27,10 +25,6 @@ export const useFindOneRecordQuery = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const findOneRecordQuery = gql`
query FindOne${capitalize(
objectMetadataItem.nameSingular,
@@ -54,7 +48,6 @@ export const useFindOneRecordQuery = ({
objectMetadataItem,
recordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
},
`;
@@ -9,9 +9,7 @@ import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useUpdateOneRecordMutation = ({
@@ -31,10 +29,6 @@ export const useUpdateOneRecordMutation = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
if (isUndefinedOrNull(objectMetadataItem)) {
return { updateOneRecordMutation: EMPTY_MUTATION };
}
@@ -60,7 +54,6 @@ export const useUpdateOneRecordMutation = ({
computeReferences,
recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
},
)}
}
@@ -9,10 +9,8 @@ import { type RecordGqlOperationSignature } from '@/object-record/graphql/types/
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import isEmpty from 'lodash.isempty';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
export const useGenerateCombinedFindManyRecordsQuery = ({
@@ -23,10 +21,6 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
if (!isNonEmptyArray(operationSignatures)) {
return null;
}
@@ -118,7 +112,6 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
objectMetadataItem,
}),
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
cursor
}
@@ -13,10 +13,8 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { type CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
import { generateCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/utils/generateCombinedFindManyRecordsQueryVariables';
import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useRecoilValue } from 'recoil';
import { capitalize } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const usePerformCombinedFindManyRecords = () => {
const apolloCoreClient = useApolloCoreClient();
@@ -24,10 +22,6 @@ export const usePerformCombinedFindManyRecords = () => {
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const generateCombinedFindManyRecordsQuery = (
operationSignatures: RecordGqlOperationSignature[],
objectMetadataItemsValue: ObjectMetadataItem[],
@@ -104,7 +98,6 @@ export const usePerformCombinedFindManyRecords = () => {
objectMetadataItem,
}),
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
cursor
}
@@ -15,7 +15,6 @@ export const generateFindManyRecordsQuery = ({
computeReferences,
cursorDirection,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
}: {
objectMetadataItem: ObjectMetadataItem;
objectMetadataItems: ObjectMetadataItem[];
@@ -26,7 +25,6 @@ export const generateFindManyRecordsQuery = ({
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
}) => gql`
query FindMany${capitalize(
objectMetadataItem.namePlural,
@@ -47,7 +45,6 @@ query FindMany${capitalize(
recordGqlFields,
computeReferences,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
})}
cursor
}
@@ -5,14 +5,12 @@ import { useObjectMetadataItemsThatCanHavePermission } from '@/settings/roles/ro
import { SettingsPath } from '@/types/SettingsPath';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { FeatureFlagKey } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledCreateObjectOverrideSection = styled(Section)`
@@ -70,16 +68,11 @@ export const SettingsRolePermissionsObjectLevelSection = ({
isDefined(objectMetadataItemsWithPermissionOverride) &&
objectMetadataItemsWithPermissionOverride?.length > 0;
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
return (
<Section>
<Table>
<SettingsRolePermissionsObjectLevelTableHeader
showPermissionsLabel={hasObjectPermissions}
isFieldsPermissionsEnabled={isFieldsPermissionsEnabled}
/>
<StyledTableRows>
{hasObjectPermissions ? (
@@ -89,7 +82,6 @@ export const SettingsRolePermissionsObjectLevelSection = ({
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
roleId={roleId}
isFieldsPermissionsEnabled={isFieldsPermissionsEnabled}
/>
),
)
@@ -1,35 +1,20 @@
import {
OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS,
OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS_WITHOUT_FIELD_PERMISSIONS,
} from '@/settings/roles/role-permissions/object-level-permissions/constants/ObjectLevelPermissionTableGridAutoColumns';
import { OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS } from '@/settings/roles/role-permissions/object-level-permissions/constants/ObjectLevelPermissionTableGridAutoColumns';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
type SettingsRolePermissionsObjectLevelTableHeaderProps = {
showPermissionsLabel?: boolean;
isFieldsPermissionsEnabled?: boolean;
};
export const SettingsRolePermissionsObjectLevelTableHeader = ({
showPermissionsLabel = true,
isFieldsPermissionsEnabled = false,
}: SettingsRolePermissionsObjectLevelTableHeaderProps) => (
<TableRow
gridAutoColumns={
isFieldsPermissionsEnabled
? OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS
: OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS_WITHOUT_FIELD_PERMISSIONS
}
>
<TableRow gridAutoColumns={OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS}>
<TableHeader>{t`Object-Level`}</TableHeader>
<TableHeader>{showPermissionsLabel ? t`Records` : ''}</TableHeader>
{isFieldsPermissionsEnabled && (
<>
<TableHeader>{showPermissionsLabel ? t`See Fields` : ''}</TableHeader>
<TableHeader>{showPermissionsLabel ? t`Edit Fields` : ''}</TableHeader>
</>
)}
<TableHeader>{showPermissionsLabel ? t`See Fields` : ''}</TableHeader>
<TableHeader>{showPermissionsLabel ? t`Edit Fields` : ''}</TableHeader>
<TableHeader></TableHeader>
</TableRow>
);
@@ -2,10 +2,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { SettingsRolePermissionsObjectLevelOverrideCellContainer } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCellContainer';
import { SettingsRolePermissionsObjectLevelSeeFieldsValueForObject } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSeeFieldsValueForObject';
import { SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject';
import {
OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS,
OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS_WITHOUT_FIELD_PERMISSIONS,
} from '@/settings/roles/role-permissions/object-level-permissions/constants/ObjectLevelPermissionTableGridAutoColumns';
import { OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS } from '@/settings/roles/role-permissions/object-level-permissions/constants/ObjectLevelPermissionTableGridAutoColumns';
import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
@@ -32,13 +29,11 @@ const StyledNameLabel = styled.div`
type SettingsRolePermissionsObjectLevelTableRowProps = {
objectMetadataItem: ObjectMetadataItem;
roleId: string;
isFieldsPermissionsEnabled: boolean;
};
export const SettingsRolePermissionsObjectLevelTableRow = ({
objectMetadataItem,
roleId,
isFieldsPermissionsEnabled,
}: SettingsRolePermissionsObjectLevelTableRowProps) => {
const { getIcon } = useIcons();
const theme = useTheme();
@@ -53,11 +48,7 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
roleId: roleId,
objectMetadataId: objectMetadataItem.id,
})}
gridAutoColumns={
isFieldsPermissionsEnabled
? OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS
: OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS_WITHOUT_FIELD_PERMISSIONS
}
gridAutoColumns={OBJECT_LEVEL_PERMISSION_TABLE_GRID_AUTO_COLUMNS}
>
<StyledNameTableCell>
{!!Icon && (
@@ -78,22 +69,18 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
objectLabel={objectLabelPlural}
/>
</TableCell>
{isFieldsPermissionsEnabled && (
<>
<TableCell>
<SettingsRolePermissionsObjectLevelSeeFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
</TableCell>
<TableCell>
<SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
</TableCell>
</>
)}
<TableCell>
<SettingsRolePermissionsObjectLevelSeeFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
</TableCell>
<TableCell>
<SettingsRolePermissionsObjectLevelUpdateFieldsValueForObject
roleId={roleId}
objectMetadataItemId={objectMetadataItem.id}
/>
</TableCell>
<TableCell align={'right'}>
<IconChevronRight
size={theme.icon.size.md}
@@ -5,11 +5,9 @@ import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/setti
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { Button } from 'twenty-ui/input';
import { FeatureFlagKey } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
type SettingsRolePermissionsObjectLevelObjectFormProps = {
@@ -25,10 +23,6 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
settingsDraftRoleFamilyState(roleId),
);
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const objectMetadata = useObjectMetadataItemById({
objectId: objectMetadataId,
});
@@ -77,12 +71,10 @@ export const SettingsRolePermissionsObjectLevelObjectForm = ({
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
{isFieldsPermissionsEnabled && (
<SettingsRolePermissionsObjectLevelObjectFieldPermissionTable
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
)}
<SettingsRolePermissionsObjectLevelObjectFieldPermissionTable
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
@@ -16,10 +16,8 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { type ViewFilter } from '@/views/types/ViewFilter';
import { relationFilterValueSchemaObject } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { ViewFilterOperand } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
const filterQueryParamsSchema = z.object({
viewId: z.string().optional(),
@@ -71,10 +69,6 @@ export const useViewFromQueryParams = () => {
isDefined(filterQueryParams) &&
Object.entries(filterQueryParams).length > 0;
const featureFlags = useFeatureFlagsMap();
const isFieldsPermissionsEnabled =
featureFlags[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
const getFiltersFromQueryParams = useRecoilCallback(
({ snapshot }) =>
async () => {
@@ -131,7 +125,6 @@ export const useViewFromQueryParams = () => {
objectMetadataItem: relationObjectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
}),
variables: {
filter: {
@@ -193,7 +186,6 @@ export const useViewFromQueryParams = () => {
objectMetadataItem.fields,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
isFieldsPermissionsEnabled,
],
);
@@ -190,38 +190,29 @@ export abstract class RestApiBaseHandler {
let restrictedFields: RestrictedFieldsPermissions = {};
if (
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED,
workspace.id,
)
) {
if (roleId) {
const objectMetadataPermissions =
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
{
workspaceId: workspace.id,
roleIds: roleId ? [roleId] : undefined,
},
);
if (roleId) {
const objectMetadataPermissions =
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
{
workspaceId: workspace.id,
roleIds: roleId ? [roleId] : undefined,
},
);
if (
!isDefined(
objectMetadataPermissions?.[roleId]?.[
objectMetadata.objectMetadataMapItem.id
]?.restrictedFields,
)
) {
throw new InternalServerError(
'Fields permissions not found for role',
);
}
restrictedFields =
objectMetadataPermissions[roleId][
if (
!isDefined(
objectMetadataPermissions?.[roleId]?.[
objectMetadata.objectMetadataMapItem.id
].restrictedFields;
]?.restrictedFields,
)
) {
throw new InternalServerError('Fields permissions not found for role');
}
restrictedFields =
objectMetadataPermissions[roleId][
objectMetadata.objectMetadataMapItem.id
].restrictedFields;
}
return {
@@ -22,15 +22,6 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
'https://twenty.com/images/lab/is-imap-smtp-caldav-enabled.png',
},
},
{
key: FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED,
metadata: {
label: 'Fields Permissions',
description: 'Configure permissions at field-level for your workspace',
imagePath:
'https://twenty.com/images/lab/is-fields-permissions-enabled.png',
},
},
{
key: FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
metadata: {
@@ -9,7 +9,6 @@ export enum FeatureFlagKey {
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED',
@@ -132,14 +132,6 @@ export class FeatureFlagService {
{ workspaceId },
);
if (featureFlag === FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED) {
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
{
workspaceId,
},
);
}
return result;
}
}
@@ -9,7 +9,6 @@ import {
import { isDefined } from 'twenty-shared/utils';
import { In, IsNull, Not, Repository } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
@@ -179,24 +178,12 @@ export class WorkspacePermissionsCacheService {
}): Promise<ObjectsPermissionsByRoleIdDeprecated> {
let roles: RoleEntity[] = [];
const workspaceFeatureFlagsMap =
await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap(
{ workspaceId },
);
const isFieldPermissionsEnabled =
workspaceFeatureFlagsMap[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
roles = await this.roleRepository.find({
where: {
workspaceId,
...(roleIds ? { id: In(roleIds) } : {}),
},
relations: [
'objectPermissions',
'permissionFlags',
...(isFieldPermissionsEnabled ? ['fieldPermissions'] : []),
],
relations: ['objectPermissions', 'permissionFlags', 'fieldPermissions'],
});
const workspaceObjectMetadataCollection =
@@ -256,22 +243,20 @@ export class WorkspacePermissionsCacheService {
canDestroy,
);
if (isFieldPermissionsEnabled) {
const fieldPermissions = role.fieldPermissions.filter(
(fieldPermission) =>
fieldPermission.objectMetadataId === objectMetadataId,
);
const fieldPermissions = role.fieldPermissions.filter(
(fieldPermission) =>
fieldPermission.objectMetadataId === objectMetadataId,
);
for (const fieldPermission of fieldPermissions) {
if (
isDefined(fieldPermission.canReadFieldValue) ||
isDefined(fieldPermission.canUpdateFieldValue)
) {
restrictedFields[fieldPermission.fieldMetadataId] = {
canRead: fieldPermission.canReadFieldValue,
canUpdate: fieldPermission.canUpdateFieldValue,
};
}
for (const fieldPermission of fieldPermissions) {
if (
isDefined(fieldPermission.canReadFieldValue) ||
isDefined(fieldPermission.canUpdateFieldValue)
) {
restrictedFields[fieldPermission.fieldMetadataId] = {
canRead: fieldPermission.canReadFieldValue,
canUpdate: fieldPermission.canUpdateFieldValue,
};
}
}
}
@@ -133,7 +133,6 @@ describe('WorkspaceEntityManager', () => {
IS_WORKFLOW_BRANCH_ENABLED: false,
IS_RELATION_CONNECT_ENABLED: false,
IS_FIELDS_PERMISSIONS_ENABLED: false,
IS_CORE_VIEW_SYNCING_ENABLED: false,
IS_CORE_VIEW_ENABLED: false,
IS_WORKSPACE_MIGRATION_V2_ENABLED: false,
@@ -158,7 +157,6 @@ describe('WorkspaceEntityManager', () => {
IS_MORPH_RELATION_ENABLED: false,
IS_RELATION_CONNECT_ENABLED: false,
IS_FIELDS_PERMISSIONS_ENABLED: true,
IS_CORE_VIEW_SYNCING_ENABLED: false,
},
permissionsPerRoleId: {},
@@ -235,10 +233,6 @@ describe('WorkspaceEntityManager', () => {
({ formattedResult }: { formattedResult: string[] }) => formattedResult,
);
jest.spyOn(entityManager as any, 'getFeatureFlagMap').mockReturnValue({
IS_FIELDS_PERMISSIONS_ENABLED: true,
});
jest
.spyOn(entityManager as any, 'extractTargetNameSingularFromEntityTarget')
.mockImplementation((entityName: string) => {
@@ -327,7 +321,6 @@ describe('WorkspaceEntityManager', () => {
});
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
entityName: 'test-entity',
isFieldPermissionsEnabled: true,
operationType: 'update',
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
objectsPermissions: mockPermissionOptions.objectRecordsPermissions,
@@ -362,7 +355,6 @@ describe('WorkspaceEntityManager', () => {
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
entityName: 'test-entity',
operationType: 'delete',
isFieldPermissionsEnabled: true,
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
objectsPermissions: mockPermissionOptions.objectRecordsPermissions,
selectedColumns: [],
@@ -410,8 +410,6 @@ export class WorkspaceEntityManager extends EntityManager {
selectedColumns,
allFieldsSelected: false,
updatedColumns,
isFieldPermissionsEnabled:
this.getFeatureFlagMap().IS_FIELDS_PERMISSIONS_ENABLED,
});
}
@@ -1214,14 +1212,11 @@ export class WorkspaceEntityManager extends EntityManager {
entities: createdEntities,
});
const isFieldPermissionsEnabled =
this.getFeatureFlagMap().IS_FIELDS_PERMISSIONS_ENABLED;
const permissionCheckApplies =
permissionOptionsFromArgs?.shouldBypassPermissionChecks !== true &&
objectMetadataItem.isSystem !== true;
if (isFieldPermissionsEnabled && permissionCheckApplies) {
if (permissionCheckApplies) {
formattedResult = this.getFormattedResultWithoutNonReadableFields({
formattedResult,
objectMetadataItem,
@@ -41,7 +41,6 @@ type ValidateOperationIsPermittedOrThrowArgs = {
objectsPermissions: ObjectsPermissionsDeprecated;
objectMetadataMaps: ObjectMetadataMaps;
selectedColumns: string[] | '*';
isFieldPermissionsEnabled?: boolean;
allFieldsSelected: boolean;
updatedColumns: string[];
};
@@ -52,7 +51,6 @@ export const validateOperationIsPermittedOrThrow = ({
objectsPermissions,
objectMetadataMaps,
selectedColumns,
isFieldPermissionsEnabled,
allFieldsSelected,
updatedColumns,
}: ValidateOperationIsPermittedOrThrowArgs) => {
@@ -81,9 +79,8 @@ export const validateOperationIsPermittedOrThrow = ({
return;
}
const columnNameToFieldMetadataIdMap = isFieldPermissionsEnabled
? getColumnNameToFieldMetadataIdMap(objectMetadata)
: {};
const columnNameToFieldMetadataIdMap =
getColumnNameToFieldMetadataIdMap(objectMetadata);
const permissionsForEntity = objectsPermissions[objectMetadataIdForEntity];
@@ -96,14 +93,12 @@ export const validateOperationIsPermittedOrThrow = ({
);
}
if (isFieldPermissionsEnabled) {
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
allFieldsSelected,
});
}
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
allFieldsSelected,
});
break;
case 'insert':
case 'update':
@@ -114,15 +109,13 @@ export const validateOperationIsPermittedOrThrow = ({
);
}
if (isFieldPermissionsEnabled) {
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
});
}
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
});
if (isFieldPermissionsEnabled && updatedColumns.length > 0) {
if (updatedColumns.length > 0) {
validateUpdateFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
updatedColumns,
@@ -138,13 +131,11 @@ export const validateOperationIsPermittedOrThrow = ({
);
}
if (isFieldPermissionsEnabled) {
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
});
}
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
});
break;
case 'restore':
case 'soft-delete':
@@ -155,13 +146,11 @@ export const validateOperationIsPermittedOrThrow = ({
);
}
if (isFieldPermissionsEnabled) {
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
});
}
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
columnNameToFieldMetadataIdMap,
});
break;
default:
throw new PermissionsException(
@@ -180,7 +169,6 @@ type ValidateQueryIsPermittedOrThrowArgs = {
objectsPermissions: ObjectsPermissionsDeprecated;
objectMetadataMaps: ObjectMetadataMaps;
shouldBypassPermissionChecks: boolean;
isFieldPermissionsEnabled?: boolean;
};
export const validateQueryIsPermittedOrThrow = ({
@@ -188,7 +176,6 @@ export const validateQueryIsPermittedOrThrow = ({
objectsPermissions,
objectMetadataMaps,
shouldBypassPermissionChecks,
isFieldPermissionsEnabled,
}: ValidateQueryIsPermittedOrThrowArgs) => {
if (shouldBypassPermissionChecks) {
return;
@@ -204,31 +191,29 @@ export const validateQueryIsPermittedOrThrow = ({
let selectedColumns: string[] | '*' = [];
let updatedColumns: string[] = [];
if (isFieldPermissionsEnabled) {
selectedColumns = getSelectedColumnsFromExpressionMap({
operationType,
expressionMap,
allFieldsSelected,
});
selectedColumns = getSelectedColumnsFromExpressionMap({
operationType,
expressionMap,
allFieldsSelected,
});
if (operationType !== 'select') {
const valuesSet = expressionMap.valuesSet;
if (operationType !== 'select') {
const valuesSet = expressionMap.valuesSet;
if (Array.isArray(valuesSet)) {
updatedColumns = valuesSet.reduce((acc, value) => {
const keys = Object.keys(value);
if (Array.isArray(valuesSet)) {
updatedColumns = valuesSet.reduce((acc, value) => {
const keys = Object.keys(value);
keys.forEach((key) => {
if (!acc.includes(key)) {
acc.push(key);
}
});
keys.forEach((key) => {
if (!acc.includes(key)) {
acc.push(key);
}
});
return acc;
}, []);
} else {
updatedColumns = Object.keys(valuesSet ?? {});
}
return acc;
}, []);
} else {
updatedColumns = Object.keys(valuesSet ?? {});
}
}
@@ -238,7 +223,6 @@ export const validateQueryIsPermittedOrThrow = ({
objectsPermissions,
objectMetadataMaps,
selectedColumns,
isFieldPermissionsEnabled,
allFieldsSelected,
updatedColumns,
});
@@ -14,7 +14,6 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
@@ -72,8 +71,6 @@ export class WorkspaceDeleteQueryBuilder<
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
@@ -12,7 +12,6 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
import { type RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
import { type RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
@@ -107,8 +106,6 @@ export class WorkspaceInsertQueryBuilder<
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
@@ -10,7 +10,6 @@ import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interf
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import {
PermissionsException,
PermissionsExceptionCode,
@@ -321,15 +320,11 @@ export class WorkspaceSelectQueryBuilder<
}
private validatePermissions(): void {
const isFieldPermissionsEnabled =
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
validateQueryIsPermittedOrThrow({
expressionMap: this.expressionMap,
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled,
});
}
@@ -12,7 +12,6 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception';
import {
TwentyORMException,
@@ -69,8 +68,6 @@ export class WorkspaceSoftDeleteQueryBuilder<
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
@@ -13,7 +13,6 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type';
import { type RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
import { type RelationDisconnectQueryFieldsByEntityIndex } from 'src/engine/twenty-orm/entity-manager/types/relation-nested-query-fields-by-entity-index.type';
@@ -91,8 +90,6 @@ export class WorkspaceUpdateQueryBuilder<
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
const mainAliasTarget = this.getMainAliasTarget();
@@ -202,8 +199,6 @@ export class WorkspaceUpdateQueryBuilder<
objectsPermissions: this.objectRecordsPermissions,
objectMetadataMaps: this.internalContext.objectMetadataMaps,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
isFieldPermissionsEnabled:
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
});
}
@@ -70,11 +70,6 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED,
workspaceId: workspaceId,
@@ -2,15 +2,11 @@ import gql from 'graphql-tag';
import { TEST_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
import { TEST_PERSON_1_ID } from 'test/integration/constants/test-person-ids.constants';
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
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 { upsertFieldPermissions } from 'test/integration/graphql/utils/upsert-field-permissions.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
import { generateRecordName } from 'test/integration/utils/generate-record-name';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
describe('Restricted fields', () => {
let personCity: string;
let memberRoleId: string;
@@ -142,313 +138,44 @@ describe('Restricted fields', () => {
});
});
describe('With Feature flag enabled', () => {
beforeAll(async () => {
const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IS_FIELDS_PERMISSIONS_ENABLED',
true,
);
it('should hide fields when user has restricted read permissions - findOne', async () => {
await makeRestAPIRequest({
method: 'get',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
})
.expect(200)
.expect((res) => {
const person = res.body.data.person;
await makeGraphqlAPIRequest(enablePermissionsQuery);
});
afterAll(async () => {
const disablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IS_FIELDS_PERMISSIONS_ENABLED',
false,
);
await makeGraphqlAPIRequest(disablePermissionsQuery);
});
it('should hide fields when user has restricted read permissions - findOne', async () => {
await makeRestAPIRequest({
method: 'get',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
})
.expect(200)
.expect((res) => {
const person = res.body.data.person;
expect(person).toBeDefined();
expect(person.id).toBeDefined();
expect(person.emails).toBeUndefined();
});
});
describe('updateOne', () => {
it('should hide fields in the response when user has restricted read permissions', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: false,
canUpdateFieldValue: null,
},
],
});
await makeRestAPIRequest({
method: 'patch',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
name: {
firstName: 'John',
},
},
})
.expect(200)
.expect((res) => {
const updatedPerson = res.body.data.updatePerson;
expect(updatedPerson.name.firstName).toBe('John');
expect(updatedPerson.phones).toBeUndefined();
});
expect(person).toBeDefined();
expect(person.id).toBeDefined();
expect(person.emails).toBeUndefined();
});
it('should block update when user tries to update non-updatable field', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: false,
},
],
});
await makeRestAPIRequest({
method: 'patch',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
phones: {
primaryPhoneNumber: '987654321',
primaryPhoneCountryCode: 'FR',
primaryPhoneCallingCode: '+33',
},
},
})
.expect(400)
.expect((res) => {
expect(res.body.messages[0]).toContain(
'Entity performing the request does not have permission',
);
});
});
it('should allow update when user has no restricted update permissions', async () => {
// Remove field permission restrictions
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: null,
},
],
});
await makeRestAPIRequest({
method: 'patch',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
city: 'Updated City',
},
})
.expect(200)
.expect((res) => {
const updatedPerson = res.body.data.updatePerson;
expect(updatedPerson.city).toBe('Updated City');
});
});
});
describe('createOne', () => {
it('should block create when user has restricted update permissions on phones field', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: false,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
phones: {
primaryPhoneNumber: '555123456',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
},
},
})
.expect(400)
.expect((res) => {
expect(res.body.messages[0]).toContain(
'Entity performing the request does not have permission',
);
});
});
it('should allow create when user has no restricted update permissions', async () => {
// Remove field permission restrictions on phones
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: null,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
city: 'New City',
},
})
.expect(201)
.expect((res) => {
const createdPerson = res.body.data.createPerson;
expect(createdPerson.city).toBe('New City');
expect(createdPerson.emails).toBeUndefined(); // No reading rights on emails
});
});
});
describe('createMany', () => {
it('should block createMany when user has restricted update permissions on phones field', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: false,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/batch/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: [
{
phones: {
primaryPhoneNumber: '555123456',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
},
},
],
})
.expect(400)
.expect((res) => {
expect(res.body.messages[0]).toContain(
'Entity performing the request does not have permission',
);
});
});
it('should allow createMany when user has no restricted update permissions', async () => {
// Remove field permission restrictions
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: null,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/batch/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: [
{
city: 'Batch City 1',
},
{
city: 'Batch City 2',
},
],
})
.expect(201)
.expect((res) => {
const createdPeople = res.body.data.createPeople;
expect(createdPeople).toHaveLength(2);
expect(createdPeople[0].city).toBe('Batch City 1');
expect(createdPeople[0].emails).toBeUndefined(); // No reading rights on emails
expect(createdPeople[1].city).toBe('Batch City 2');
expect(createdPeople[1].emails).toBeUndefined(); // No reading rights on emails
});
});
});
});
describe('With feature flag disabled', () => {
it('should query all fields despite field permission restriction', async () => {
await makeRestAPIRequest({
method: 'get',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
})
.expect(200)
.expect((res) => {
const person = res.body.data.person;
describe('updateOne', () => {
it('should hide fields in the response when user has restricted read permissions', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: false,
canUpdateFieldValue: null,
},
],
});
expect(person).toBeDefined();
expect(person.id).toBeDefined();
expect(person.emails).toBeDefined();
});
});
it('should allow updates despite field permission restriction', async () => {
await makeRestAPIRequest({
method: 'patch',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
phones: {
primaryPhoneNumber: '111222333',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
name: {
firstName: 'John',
},
},
})
@@ -456,7 +183,215 @@ describe('Restricted fields', () => {
.expect((res) => {
const updatedPerson = res.body.data.updatePerson;
expect(updatedPerson.phones.primaryPhoneNumber).toBe('111222333');
expect(updatedPerson.name.firstName).toBe('John');
expect(updatedPerson.phones).toBeUndefined();
});
});
it('should block update when user tries to update non-updatable field', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: false,
},
],
});
await makeRestAPIRequest({
method: 'patch',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
phones: {
primaryPhoneNumber: '987654321',
primaryPhoneCountryCode: 'FR',
primaryPhoneCallingCode: '+33',
},
},
})
.expect(400)
.expect((res) => {
expect(res.body.messages[0]).toContain(
'Entity performing the request does not have permission',
);
});
});
it('should allow update when user has no restricted update permissions', async () => {
// Remove field permission restrictions
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: null,
},
],
});
await makeRestAPIRequest({
method: 'patch',
path: `/people/${TEST_PERSON_1_ID}`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
city: 'Updated City',
},
})
.expect(200)
.expect((res) => {
const updatedPerson = res.body.data.updatePerson;
expect(updatedPerson.city).toBe('Updated City');
});
});
});
describe('createOne', () => {
it('should block create when user has restricted update permissions on phones field', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: false,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
phones: {
primaryPhoneNumber: '555123456',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
},
},
})
.expect(400)
.expect((res) => {
expect(res.body.messages[0]).toContain(
'Entity performing the request does not have permission',
);
});
});
it('should allow create when user has no restricted update permissions', async () => {
// Remove field permission restrictions on phones
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: null,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: {
city: 'New City',
},
})
.expect(201)
.expect((res) => {
const createdPerson = res.body.data.createPerson;
expect(createdPerson.city).toBe('New City');
expect(createdPerson.emails).toBeUndefined(); // No reading rights on emails
});
});
});
describe('createMany', () => {
it('should block createMany when user has restricted update permissions on phones field', async () => {
// Create field permission restricting update access to phones field
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: false,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/batch/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: [
{
phones: {
primaryPhoneNumber: '555123456',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
},
},
],
})
.expect(400)
.expect((res) => {
expect(res.body.messages[0]).toContain(
'Entity performing the request does not have permission',
);
});
});
it('should allow createMany when user has no restricted update permissions', async () => {
// Remove field permission restrictions
await upsertFieldPermissions({
roleId: memberRoleId,
fieldPermissions: [
{
objectMetadataId: personObjectId,
fieldMetadataId: phonesFieldId,
canReadFieldValue: null,
canUpdateFieldValue: null,
},
],
});
await makeRestAPIRequest({
method: 'post',
path: `/batch/people`,
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
body: [
{
city: 'Batch City 1',
},
{
city: 'Batch City 2',
},
],
})
.expect(201)
.expect((res) => {
const createdPeople = res.body.data.createPeople;
expect(createdPeople).toHaveLength(2);
expect(createdPeople[0].city).toBe('Batch City 1');
expect(createdPeople[0].emails).toBeUndefined(); // No reading rights on emails
expect(createdPeople[1].city).toBe('Batch City 2');
expect(createdPeople[1].emails).toBeUndefined(); // No reading rights on emails
});
});
});