3bec43696f
Implements permission intersection (AND logic) to prevent permission escalation when agents act on behalf of users. ### Changes: - **Permission Intersection**: Operations requiring both user AND agent permissions - **RoleContext Type**: Unified type supporting single `roleId` or multiple `roleIds` for intersection - **CRUD Services**: Updated to accept `roleContext` for granular permission control - **Agent Integration**: Chat agents now use user + agent role intersection for all operations - **ORM Layer**: Enhanced `getRepository` to support multi-role permission checks ### Related: - Part 2 of ["Acting on behalf of user" concept PR](https://github.com/twentyhq/twenty/pull/15103) [Closes #1661](https://github.com/twentyhq/core-team-issues/issues/1661) --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
import {
|
|
type ObjectsPermissions,
|
|
type RestrictedFieldPermissions,
|
|
} from 'twenty-shared/types';
|
|
|
|
export const computePermissionIntersection = (
|
|
permissionsArray: ObjectsPermissions[],
|
|
): ObjectsPermissions => {
|
|
if (permissionsArray.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
if (permissionsArray.length === 1) {
|
|
return permissionsArray[0];
|
|
}
|
|
|
|
const result: ObjectsPermissions = {};
|
|
|
|
const allObjectMetadataIds = new Set<string>();
|
|
|
|
for (const permissions of permissionsArray) {
|
|
for (const id of Object.keys(permissions)) {
|
|
allObjectMetadataIds.add(id);
|
|
}
|
|
}
|
|
|
|
for (const objectMetadataId of allObjectMetadataIds) {
|
|
let canReadObjectRecords = true;
|
|
let canUpdateObjectRecords = true;
|
|
let canSoftDeleteObjectRecords = true;
|
|
let canDestroyObjectRecords = true;
|
|
const restrictedFields: Record<string, RestrictedFieldPermissions> = {};
|
|
|
|
for (const permissions of permissionsArray) {
|
|
const objPerm = permissions[objectMetadataId];
|
|
|
|
if (!objPerm) {
|
|
canReadObjectRecords = false;
|
|
canUpdateObjectRecords = false;
|
|
canSoftDeleteObjectRecords = false;
|
|
canDestroyObjectRecords = false;
|
|
continue;
|
|
}
|
|
|
|
canReadObjectRecords =
|
|
canReadObjectRecords && objPerm.canReadObjectRecords === true;
|
|
canUpdateObjectRecords =
|
|
canUpdateObjectRecords && objPerm.canUpdateObjectRecords === true;
|
|
canSoftDeleteObjectRecords =
|
|
canSoftDeleteObjectRecords &&
|
|
objPerm.canSoftDeleteObjectRecords === true;
|
|
canDestroyObjectRecords =
|
|
canDestroyObjectRecords && objPerm.canDestroyObjectRecords === true;
|
|
|
|
if (objPerm.restrictedFields) {
|
|
for (const [fieldName, fieldPerm] of Object.entries(
|
|
objPerm.restrictedFields,
|
|
)) {
|
|
if (!restrictedFields[fieldName]) {
|
|
restrictedFields[fieldName] = {
|
|
canRead: null,
|
|
canUpdate: null,
|
|
};
|
|
}
|
|
|
|
const current = restrictedFields[fieldName];
|
|
|
|
restrictedFields[fieldName] = {
|
|
canRead:
|
|
current.canRead === false || fieldPerm.canRead === false
|
|
? false
|
|
: null,
|
|
canUpdate:
|
|
current.canUpdate === false || fieldPerm.canUpdate === false
|
|
? false
|
|
: null,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
result[objectMetadataId] = {
|
|
canReadObjectRecords,
|
|
canUpdateObjectRecords,
|
|
canSoftDeleteObjectRecords,
|
|
canDestroyObjectRecords,
|
|
restrictedFields,
|
|
};
|
|
}
|
|
|
|
return result;
|
|
};
|