feat: refactoring casl permission checks for recursive nested operations (#778)
* feat: nested casl abilities * fix: remove unused packages * Fixes * Fix createMany broken * Fix lint * Fix lint * Fix lint * Fix lint * Fixes * Fix CommentThread * Fix bugs * Fix lint * Fix bugs * Fixed auto routing * Fixed app path --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
207
server/src/ability/ability.util.ts
Normal file
207
server/src/ability/ability.util.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
import { subject } from '@casl/ability';
|
||||
|
||||
import { camelCase } from 'src/utils/camel-case';
|
||||
|
||||
import { AppAbility } from './ability.factory';
|
||||
import { AbilityAction } from './ability.action';
|
||||
|
||||
type OperationType =
|
||||
| 'create'
|
||||
| 'connectOrCreate'
|
||||
| 'upsert'
|
||||
| 'createMany'
|
||||
| 'set'
|
||||
| 'disconnect'
|
||||
| 'delete'
|
||||
| 'connect'
|
||||
| 'update'
|
||||
| 'updateMany'
|
||||
| 'deleteMany';
|
||||
|
||||
// in most case unique identifier is the id, but it can be something else...
|
||||
|
||||
type OperationAbilityChecker = (
|
||||
modelName: Prisma.ModelName,
|
||||
ability: AppAbility,
|
||||
prisma: PrismaClient,
|
||||
data: any,
|
||||
) => Promise<boolean>;
|
||||
|
||||
const createAbilityCheck: OperationAbilityChecker = async (
|
||||
modelName,
|
||||
ability,
|
||||
prisma,
|
||||
data,
|
||||
) => {
|
||||
// Handle all operations cases
|
||||
const items = data?.data
|
||||
? !Array.isArray(data.data)
|
||||
? [data.data]
|
||||
: data.data
|
||||
: !Array.isArray(data)
|
||||
? [data]
|
||||
: data;
|
||||
|
||||
// Check if user try to create an element that is not allowed to create
|
||||
for (const {} of items) {
|
||||
if (!ability.can(AbilityAction.Create, modelName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const simpleAbilityCheck: OperationAbilityChecker = async (
|
||||
modelName,
|
||||
ability,
|
||||
prisma,
|
||||
data,
|
||||
) => {
|
||||
// Extract entity name from model name
|
||||
const entity = camelCase(modelName);
|
||||
// Handle all operations cases
|
||||
const operations = !Array.isArray(data) ? [data] : data;
|
||||
// Handle where case
|
||||
const normalizedOperations = operations.map((op) =>
|
||||
op.where ? op.where : op,
|
||||
);
|
||||
// Force entity type because of Prisma typing
|
||||
const items = await prisma[entity as string].findMany({
|
||||
where: {
|
||||
OR: normalizedOperations,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if user try to connect an element that is not allowed to read
|
||||
for (const item of items) {
|
||||
// TODO: Replace user by workspaceMember and remove this check
|
||||
if (
|
||||
modelName === 'User' ||
|
||||
modelName === 'UserSettings' ||
|
||||
modelName === 'Workspace'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ability.can(AbilityAction.Read, subject(modelName, item))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const operationAbilityCheckers: Record<OperationType, OperationAbilityChecker> =
|
||||
{
|
||||
create: createAbilityCheck,
|
||||
createMany: createAbilityCheck,
|
||||
upsert: simpleAbilityCheck,
|
||||
update: simpleAbilityCheck,
|
||||
updateMany: simpleAbilityCheck,
|
||||
delete: simpleAbilityCheck,
|
||||
deleteMany: simpleAbilityCheck,
|
||||
connectOrCreate: simpleAbilityCheck,
|
||||
connect: simpleAbilityCheck,
|
||||
disconnect: simpleAbilityCheck,
|
||||
set: simpleAbilityCheck,
|
||||
};
|
||||
|
||||
// Check relation nested abilities
|
||||
export async function relationAbilityChecker(
|
||||
modelName: Prisma.ModelName,
|
||||
ability: AppAbility,
|
||||
prisma: PrismaClient,
|
||||
args: any,
|
||||
) {
|
||||
// Extract models from Prisma
|
||||
const models = Prisma.dmmf.datamodel.models;
|
||||
// Find main model from options
|
||||
const mainModel = models.find((item) => item.name === modelName);
|
||||
|
||||
if (!mainModel) {
|
||||
throw new Error('Main model not found');
|
||||
}
|
||||
|
||||
// Loop over fields
|
||||
for (const field of mainModel.fields) {
|
||||
// Check if field is a relation
|
||||
if (field.relationName) {
|
||||
// Check if field is in args
|
||||
const operation = args.data?.[field.name] ?? args?.[field.name];
|
||||
|
||||
if (operation) {
|
||||
// Extract operation name and value
|
||||
const operationType = Object.keys(operation)[0] as OperationType;
|
||||
const operationValue = operation[operationType];
|
||||
|
||||
// Get operation checker for the operation type
|
||||
const operationChecker = operationAbilityCheckers[operationType];
|
||||
|
||||
if (!operationChecker) {
|
||||
throw new Error('Operation not found');
|
||||
}
|
||||
|
||||
// Check if operation is allowed
|
||||
const allowed = await operationChecker(
|
||||
field.type as Prisma.ModelName,
|
||||
ability,
|
||||
prisma,
|
||||
operationValue,
|
||||
);
|
||||
|
||||
if (!allowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For the 'create', 'connectOrCreate', 'upsert', 'update', and 'updateMany' operations,
|
||||
// we should also check the nested operations.
|
||||
if (
|
||||
[
|
||||
'create',
|
||||
'connectOrCreate',
|
||||
'upsert',
|
||||
'update',
|
||||
'updateMany',
|
||||
].includes(operationType)
|
||||
) {
|
||||
// Handle nested operations all cases
|
||||
|
||||
const operationValues = !Array.isArray(operationValue)
|
||||
? [operationValue]
|
||||
: operationValue;
|
||||
|
||||
// Loop over nested args
|
||||
for (const nestedArgs of operationValues) {
|
||||
const nestedCreateAllowed = await relationAbilityChecker(
|
||||
field.type as Prisma.ModelName,
|
||||
ability,
|
||||
prisma,
|
||||
nestedArgs.create ?? nestedArgs.data ?? nestedArgs,
|
||||
);
|
||||
|
||||
if (!nestedCreateAllowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nestedArgs.update) {
|
||||
const nestedUpdateAllowed = await relationAbilityChecker(
|
||||
field.type as Prisma.ModelName,
|
||||
ability,
|
||||
prisma,
|
||||
nestedArgs.update,
|
||||
);
|
||||
|
||||
if (!nestedUpdateAllowed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user