867619247f
Fixes https://github.com/twentyhq/twenty/issues/12867 Issue: when you have a variable `toto` which is: `Record<string, MyType>` and you do toto['xxx'], this will be typed as `MyType` instead of `MyType | undefined` Solutions: - activate `noUncheckedIndexedAccess` check in tsconfig, this is the preferred solution but will take time to get there (this raises 600+ errors) - use a Map: cf https://github.com/twentyhq/twenty/pull/13125/files - set the type to Partial<Record<string, MyType>>. Drawback is that when you do Object.values(toto), you'll get `Array<MyType | undefined>`. Hence why we have to filter these behind <img width="1512" alt="image" src="https://github.com/user-attachments/assets/d0a0bfed-c441-4e53-84c2-2da98ccbcf50" />
133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
import { isNonEmptyString } from '@sniptt/guards';
|
|
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
|
|
|
|
import {
|
|
PermissionsException,
|
|
PermissionsExceptionCode,
|
|
PermissionsExceptionMessage,
|
|
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
|
|
|
const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
|
|
const mainEntity = expressionMap.aliases[0].metadata.name;
|
|
const operationType = expressionMap.queryType;
|
|
|
|
return {
|
|
mainEntity,
|
|
operationType,
|
|
};
|
|
};
|
|
|
|
export type OperationType =
|
|
| 'select'
|
|
| 'insert'
|
|
| 'update'
|
|
| 'delete'
|
|
| 'restore'
|
|
| 'soft-delete';
|
|
|
|
export const validateOperationIsPermittedOrThrow = ({
|
|
entityName,
|
|
operationType,
|
|
objectRecordsPermissions,
|
|
objectMetadataMaps,
|
|
}: {
|
|
entityName: string;
|
|
operationType: OperationType;
|
|
objectRecordsPermissions: ObjectRecordsPermissions;
|
|
objectMetadataMaps: ObjectMetadataMaps;
|
|
}) => {
|
|
const objectMetadataIdForEntity =
|
|
objectMetadataMaps.idByNameSingular[entityName];
|
|
|
|
if (!isNonEmptyString(objectMetadataIdForEntity)) {
|
|
throw new PermissionsException(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
|
);
|
|
}
|
|
|
|
const objectMetadata = objectMetadataMaps.byId[objectMetadataIdForEntity];
|
|
|
|
if (!isDefined(objectMetadata)) {
|
|
throw new PermissionsException(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
|
);
|
|
}
|
|
|
|
const objectMetadataIsSystem = objectMetadata.isSystem === true;
|
|
|
|
if (objectMetadataIsSystem) {
|
|
return;
|
|
}
|
|
|
|
const permissionsForEntity =
|
|
objectRecordsPermissions[objectMetadataIdForEntity];
|
|
|
|
switch (operationType) {
|
|
case 'select':
|
|
if (!permissionsForEntity?.canRead) {
|
|
throw new PermissionsException(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
|
);
|
|
}
|
|
break;
|
|
case 'insert':
|
|
case 'update':
|
|
if (!permissionsForEntity?.canUpdate) {
|
|
throw new PermissionsException(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
|
);
|
|
}
|
|
break;
|
|
case 'delete':
|
|
if (!permissionsForEntity?.canDestroy) {
|
|
throw new PermissionsException(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
|
);
|
|
}
|
|
break;
|
|
case 'restore':
|
|
case 'soft-delete':
|
|
if (!permissionsForEntity?.canSoftDelete) {
|
|
throw new PermissionsException(
|
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
|
);
|
|
}
|
|
break;
|
|
default:
|
|
throw new PermissionsException(
|
|
PermissionsExceptionMessage.UNKNOWN_OPERATION_NAME,
|
|
PermissionsExceptionCode.UNKNOWN_OPERATION_NAME,
|
|
);
|
|
}
|
|
};
|
|
|
|
export const validateQueryIsPermittedOrThrow = (
|
|
expressionMap: QueryExpressionMap,
|
|
objectRecordsPermissions: ObjectRecordsPermissions,
|
|
objectMetadataMaps: ObjectMetadataMaps,
|
|
shouldBypassPermissionChecks: boolean,
|
|
) => {
|
|
if (shouldBypassPermissionChecks) {
|
|
return;
|
|
}
|
|
|
|
const { mainEntity, operationType } =
|
|
getTargetEntityAndOperationType(expressionMap);
|
|
|
|
validateOperationIsPermittedOrThrow({
|
|
entityName: mainEntity,
|
|
operationType: operationType as OperationType,
|
|
objectRecordsPermissions,
|
|
objectMetadataMaps,
|
|
});
|
|
};
|