[permissions] Update permission check layer (#13485)

Fixes https://github.com/twentyhq/core-team-issues/issues/1262

In this PR we add the update permission check layer by 
- for the graphql api: extracting columns to update from the
expressionMap
- for rest api: .save() is used so we need to add the permission layer
to .save directly. We also take advantage of this PR to filter out
non-readable fields from save response (other save returns the whole
entity) - this was planned in
https://github.com/twentyhq/core-team-issues/issues/1216

The current solution does not work with rest api depth 2 queries, but
this seem to already not work on main (for timeout reasons though, so
different). I offer to create a ticket to fix it altogether later.
This commit is contained in:
Marie
2025-07-31 18:37:01 +02:00
committed by GitHub
parent 3e9b642f7f
commit b41502a4b8
20 changed files with 1483 additions and 124 deletions
@@ -15,7 +15,7 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getFieldMetadataIdForColumnNameMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-for-column-name.util';
import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util';
const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
const mainEntity = expressionMap.aliases[0].metadata.name;
@@ -43,6 +43,7 @@ export const validateOperationIsPermittedOrThrow = ({
selectedColumns,
isFieldPermissionsEnabled,
allFieldsSelected,
updatedColumns,
}: {
entityName: string;
operationType: OperationType;
@@ -51,6 +52,7 @@ export const validateOperationIsPermittedOrThrow = ({
selectedColumns: string[];
isFieldPermissionsEnabled?: boolean;
allFieldsSelected: boolean;
updatedColumns: string[];
}) => {
const objectMetadataIdForEntity =
objectMetadataMaps.idByNameSingular[entityName];
@@ -77,8 +79,8 @@ export const validateOperationIsPermittedOrThrow = ({
return;
}
const fieldMetadataIdForColumnNameMap = isFieldPermissionsEnabled
? getFieldMetadataIdForColumnNameMap(objectMetadata)
const columnNameToFieldMetadataIdMap = isFieldPermissionsEnabled
? getColumnNameToFieldMetadataIdMap(objectMetadata)
: {};
const permissionsForEntity =
@@ -97,7 +99,7 @@ export const validateOperationIsPermittedOrThrow = ({
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
fieldMetadataIdForColumnNameMap,
columnNameToFieldMetadataIdMap,
allFieldsSelected,
});
}
@@ -115,7 +117,15 @@ export const validateOperationIsPermittedOrThrow = ({
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
fieldMetadataIdForColumnNameMap,
columnNameToFieldMetadataIdMap,
});
}
if (isFieldPermissionsEnabled && updatedColumns.length > 0) {
validateUpdateFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
updatedColumns,
columnNameToFieldMetadataIdMap,
});
}
break;
@@ -131,7 +141,7 @@ export const validateOperationIsPermittedOrThrow = ({
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
fieldMetadataIdForColumnNameMap,
columnNameToFieldMetadataIdMap,
});
}
break;
@@ -148,7 +158,7 @@ export const validateOperationIsPermittedOrThrow = ({
validateReadFieldPermissionOrThrow({
restrictedFields: permissionsForEntity.restrictedFields,
selectedColumns,
fieldMetadataIdForColumnNameMap,
columnNameToFieldMetadataIdMap,
});
}
break;
@@ -189,6 +199,7 @@ export const validateQueryIsPermittedOrThrow = ({
);
let selectedColumns: string[] = [];
let updatedColumns: string[] = [];
if (isFieldPermissionsEnabled) {
selectedColumns = getSelectedColumnsFromExpressionMap({
@@ -196,6 +207,26 @@ export const validateQueryIsPermittedOrThrow = ({
expressionMap,
allFieldsSelected,
});
if (operationType !== 'select') {
const valuesSet = expressionMap.valuesSet;
if (Array.isArray(valuesSet)) {
updatedColumns = valuesSet.reduce((acc, value) => {
const keys = Object.keys(value);
keys.forEach((key) => {
if (!acc.includes(key)) {
acc.push(key);
}
});
return acc;
}, []);
} else {
updatedColumns = Object.keys(valuesSet ?? {});
}
}
}
validateOperationIsPermittedOrThrow({
@@ -203,21 +234,22 @@ export const validateQueryIsPermittedOrThrow = ({
operationType: operationType as OperationType,
objectRecordsPermissions,
objectMetadataMaps,
selectedColumns: selectedColumns,
selectedColumns,
isFieldPermissionsEnabled,
allFieldsSelected,
updatedColumns,
});
};
const validateReadFieldPermissionOrThrow = ({
restrictedFields,
selectedColumns,
fieldMetadataIdForColumnNameMap,
columnNameToFieldMetadataIdMap,
allFieldsSelected,
}: {
restrictedFields: RestrictedFields;
selectedColumns: string[];
fieldMetadataIdForColumnNameMap: Record<string, string>;
columnNameToFieldMetadataIdMap: Record<string, string>;
allFieldsSelected?: boolean;
}) => {
if (isEmpty(restrictedFields)) {
@@ -232,7 +264,7 @@ const validateReadFieldPermissionOrThrow = ({
}
for (const column of selectedColumns) {
const fieldMetadataId = fieldMetadataIdForColumnNameMap[column];
const fieldMetadataId = columnNameToFieldMetadataIdMap[column];
if (!fieldMetadataId) {
throw new InternalServerError(
@@ -249,6 +281,37 @@ const validateReadFieldPermissionOrThrow = ({
}
};
const validateUpdateFieldPermissionOrThrow = ({
restrictedFields,
updatedColumns,
columnNameToFieldMetadataIdMap,
}: {
restrictedFields: RestrictedFields;
updatedColumns: string[];
columnNameToFieldMetadataIdMap: Record<string, string>;
}) => {
if (isEmpty(restrictedFields)) {
return;
}
for (const column of updatedColumns) {
const fieldMetadataId = columnNameToFieldMetadataIdMap[column];
if (!fieldMetadataId) {
throw new InternalServerError(
`Field metadata id not found for column name ${column}`,
);
}
if (restrictedFields[fieldMetadataId]?.canUpdate === false) {
throw new PermissionsException(
PermissionsExceptionMessage.PERMISSION_DENIED,
PermissionsExceptionCode.PERMISSION_DENIED,
);
}
}
};
const getSelectedColumnsFromExpressionMap = ({
operationType,
expressionMap,