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