[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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user