diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ce590..52b7660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ +## [0.6.0](https://github.com/dennemark/prisma-extension-casl/compare/0.5.20...0.6.0) (2024-09-24) + + +### Features + +* :sparkles: add possibility to store simplified crud rights on model ([7130a33](https://github.com/dennemark/prisma-extension-casl/commit/7130a3367c9fe0810cc5406f861eee1e0991098c)) + ## [0.5.20](https://github.com/dennemark/prisma-extension-casl/compare/0.5.19...0.5.20) (2024-09-23) diff --git a/dist/index.d.mts b/dist/index.d.mts index ff09ae9..bf4baee 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts @@ -20,7 +20,7 @@ type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'up * @param model Prisma model * @returns Enriched query with casl authorization */ -declare function applyCaslToQuery(operation: PrismaCaslOperation, args: any, abilities: PureAbility, model: Prisma.ModelName): { +declare function applyCaslToQuery(operation: PrismaCaslOperation, args: any, abilities: PureAbility, model: Prisma.ModelName, queryAllRuleRelations?: boolean): { creationTree: CreationTree | undefined; args: any; mask: Record; @@ -42,7 +42,7 @@ declare function applyCaslToQuery(operation: PrismaCaslOperation, args: any, abi * - this is a function call to instantiate abilities on each prisma query to allow adding i.e. context or claims * @returns enriched prisma client */ -declare function useCaslAbilities(getAbilityFactory: () => AbilityBuilder>): (client: any) => { +declare function useCaslAbilities(getAbilityFactory: () => AbilityBuilder>, permissionField?: string): (client: any) => { $extends: { extArgs: { result: {}; diff --git a/dist/index.d.ts b/dist/index.d.ts index ff09ae9..bf4baee 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -20,7 +20,7 @@ type PrismaCaslOperation = 'create' | 'createMany' | 'createManyAndReturn' | 'up * @param model Prisma model * @returns Enriched query with casl authorization */ -declare function applyCaslToQuery(operation: PrismaCaslOperation, args: any, abilities: PureAbility, model: Prisma.ModelName): { +declare function applyCaslToQuery(operation: PrismaCaslOperation, args: any, abilities: PureAbility, model: Prisma.ModelName, queryAllRuleRelations?: boolean): { creationTree: CreationTree | undefined; args: any; mask: Record; @@ -42,7 +42,7 @@ declare function applyCaslToQuery(operation: PrismaCaslOperation, args: any, abi * - this is a function call to instantiate abilities on each prisma query to allow adding i.e. context or claims * @returns enriched prisma client */ -declare function useCaslAbilities(getAbilityFactory: () => AbilityBuilder>): (client: any) => { +declare function useCaslAbilities(getAbilityFactory: () => AbilityBuilder>, permissionField?: string): (client: any) => { $extends: { extArgs: { result: {}; diff --git a/dist/index.js b/dist/index.js index d570833..6a82a58 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1177,6 +1177,7 @@ function getNestedQueryRelations(args, abilities, action, model, creationSelectQ const ability = e3(abilities.rules.filter((rule) => rule.conditions).map((rule) => { return { ...rule, + action: action === "all" ? action : rule.action, inverted: false }; })); @@ -1239,7 +1240,7 @@ function transformDataToWhereQuery(args, model) { } // src/applyCaslToQuery.ts -function applyCaslToQuery(operation, args, abilities, model) { +function applyCaslToQuery(operation, args, abilities, model, queryAllRuleRelations) { const operationAbility = caslOperationDict[operation]; m5(abilities, operationAbility.action)[model]; let creationTree = void 0; @@ -1263,12 +1264,35 @@ function applyCaslToQuery(operation, args, abilities, model) { delete args.include; delete args.select; } - const result = operationAbility.includeSelectQuery ? applyRuleRelationsQuery(args, abilities, operationAbility.action, model, creationTree) : { args, mask: void 0, creationTree }; + const result = operationAbility.includeSelectQuery ? applyRuleRelationsQuery(args, abilities, queryAllRuleRelations ? "all" : operationAbility.action, model, creationTree) : { args, mask: void 0, creationTree }; return result; } +// src/storePermissions.ts +function storePermissions(result, abilities, model, prop) { + if (prop === void 0) { + return result; + } + const actions = ["create", "read", "update", "delete"]; + const storeProp = (entry) => { + entry[prop] = []; + actions.forEach((action) => { + if (abilities.can(action, getSubject(model, entry))) { + entry[prop].push(action); + } + }); + return entry; + }; + if (Array.isArray(result)) { + const res = result.map(storeProp); + return res; + } else { + return storeProp(result); + } +} + // src/filterQueryResults.ts -function filterQueryResults(result, mask, creationTree, abilities, model) { +function filterQueryResults(result, mask, creationTree, abilities, model, permissionField) { if (typeof result === "number") { return result; } @@ -1291,7 +1315,7 @@ function filterQueryResults(result, mask, creationTree, abilities, model) { } const permittedFields = getPermittedFields(abilities, "read", model, entry); let hasKeys = false; - Object.keys(entry).forEach((field) => { + Object.keys(entry).filter((field) => field !== permissionField).forEach((field) => { const relationField = relationFieldsByModel[model][field]; if (relationField) { const nestedCreationTree = creationTree && field in creationTree.children ? creationTree.children[field] : void 0; @@ -1311,15 +1335,16 @@ function filterQueryResults(result, mask, creationTree, abilities, model) { }); return hasKeys && Object.keys(entry).length > 0 ? entry : null; }; - if (Array.isArray(result)) { - return result.map((entry) => filterPermittedFields(entry)).filter((x5) => x5); + const permissionResult = storePermissions(result, abilities, model, permissionField); + if (Array.isArray(permissionResult)) { + return permissionResult.map((entry) => filterPermittedFields(entry)).filter((x5) => x5); } else { - return filterPermittedFields(result); + return filterPermittedFields(permissionResult); } } // src/index.ts -function useCaslAbilities(getAbilityFactory) { +function useCaslAbilities(getAbilityFactory, permissionField) { return import_client2.Prisma.defineExtension((client) => { let getAbilities = () => getAbilityFactory(); return client.$extends({ @@ -1366,13 +1391,13 @@ function useCaslAbilities(getAbilityFactory) { } getAbilities = () => getAbilityFactory(); perf?.mark("prisma-casl-extension-1"); - const caslQuery = applyCaslToQuery(operation, args, abilities, model); + const caslQuery = applyCaslToQuery(operation, args, abilities, model, permissionField ? true : false); perf?.mark("prisma-casl-extension-2"); logger?.log("Query Args", JSON.stringify(caslQuery.args)); logger?.log("Query Mask", JSON.stringify(caslQuery.mask)); const cleanupResults = (result) => { perf?.mark("prisma-casl-extension-3"); - const res = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, getFluentModel(model, rest)); + const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, getFluentModel(model, rest), permissionField); if (perf) { perf.mark("prisma-casl-extension-4"); logger?.log( @@ -1387,7 +1412,7 @@ function useCaslAbilities(getAbilityFactory) { }) ); } - return operation === "createMany" ? { count: res.length } : res; + return operation === "createMany" ? { count: filteredResult.length } : filteredResult; }; const operationAbility = caslOperationDict[operation]; if (operationAbility.action === "update" || operationAbility.action === "create") { diff --git a/dist/index.mjs b/dist/index.mjs index 8e1d9a3..13ca562 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -1152,6 +1152,7 @@ function getNestedQueryRelations(args, abilities, action, model, creationSelectQ const ability = e3(abilities.rules.filter((rule) => rule.conditions).map((rule) => { return { ...rule, + action: action === "all" ? action : rule.action, inverted: false }; })); @@ -1214,7 +1215,7 @@ function transformDataToWhereQuery(args, model) { } // src/applyCaslToQuery.ts -function applyCaslToQuery(operation, args, abilities, model) { +function applyCaslToQuery(operation, args, abilities, model, queryAllRuleRelations) { const operationAbility = caslOperationDict[operation]; m5(abilities, operationAbility.action)[model]; let creationTree = void 0; @@ -1238,12 +1239,35 @@ function applyCaslToQuery(operation, args, abilities, model) { delete args.include; delete args.select; } - const result = operationAbility.includeSelectQuery ? applyRuleRelationsQuery(args, abilities, operationAbility.action, model, creationTree) : { args, mask: void 0, creationTree }; + const result = operationAbility.includeSelectQuery ? applyRuleRelationsQuery(args, abilities, queryAllRuleRelations ? "all" : operationAbility.action, model, creationTree) : { args, mask: void 0, creationTree }; return result; } +// src/storePermissions.ts +function storePermissions(result, abilities, model, prop) { + if (prop === void 0) { + return result; + } + const actions = ["create", "read", "update", "delete"]; + const storeProp = (entry) => { + entry[prop] = []; + actions.forEach((action) => { + if (abilities.can(action, getSubject(model, entry))) { + entry[prop].push(action); + } + }); + return entry; + }; + if (Array.isArray(result)) { + const res = result.map(storeProp); + return res; + } else { + return storeProp(result); + } +} + // src/filterQueryResults.ts -function filterQueryResults(result, mask, creationTree, abilities, model) { +function filterQueryResults(result, mask, creationTree, abilities, model, permissionField) { if (typeof result === "number") { return result; } @@ -1266,7 +1290,7 @@ function filterQueryResults(result, mask, creationTree, abilities, model) { } const permittedFields = getPermittedFields(abilities, "read", model, entry); let hasKeys = false; - Object.keys(entry).forEach((field) => { + Object.keys(entry).filter((field) => field !== permissionField).forEach((field) => { const relationField = relationFieldsByModel[model][field]; if (relationField) { const nestedCreationTree = creationTree && field in creationTree.children ? creationTree.children[field] : void 0; @@ -1286,15 +1310,16 @@ function filterQueryResults(result, mask, creationTree, abilities, model) { }); return hasKeys && Object.keys(entry).length > 0 ? entry : null; }; - if (Array.isArray(result)) { - return result.map((entry) => filterPermittedFields(entry)).filter((x5) => x5); + const permissionResult = storePermissions(result, abilities, model, permissionField); + if (Array.isArray(permissionResult)) { + return permissionResult.map((entry) => filterPermittedFields(entry)).filter((x5) => x5); } else { - return filterPermittedFields(result); + return filterPermittedFields(permissionResult); } } // src/index.ts -function useCaslAbilities(getAbilityFactory) { +function useCaslAbilities(getAbilityFactory, permissionField) { return Prisma2.defineExtension((client) => { let getAbilities = () => getAbilityFactory(); return client.$extends({ @@ -1341,13 +1366,13 @@ function useCaslAbilities(getAbilityFactory) { } getAbilities = () => getAbilityFactory(); perf?.mark("prisma-casl-extension-1"); - const caslQuery = applyCaslToQuery(operation, args, abilities, model); + const caslQuery = applyCaslToQuery(operation, args, abilities, model, permissionField ? true : false); perf?.mark("prisma-casl-extension-2"); logger?.log("Query Args", JSON.stringify(caslQuery.args)); logger?.log("Query Mask", JSON.stringify(caslQuery.mask)); const cleanupResults = (result) => { perf?.mark("prisma-casl-extension-3"); - const res = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, getFluentModel(model, rest)); + const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, getFluentModel(model, rest), permissionField); if (perf) { perf.mark("prisma-casl-extension-4"); logger?.log( @@ -1362,7 +1387,7 @@ function useCaslAbilities(getAbilityFactory) { }) ); } - return operation === "createMany" ? { count: res.length } : res; + return operation === "createMany" ? { count: filteredResult.length } : filteredResult; }; const operationAbility = caslOperationDict[operation]; if (operationAbility.action === "update" || operationAbility.action === "create") { diff --git a/package.json b/package.json index caf8e9a..663fd59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prisma-extension-casl", - "version": "0.5.20", + "version": "0.6.0", "description": "Enforce casl abilities on prisma client ", "main": "dist/index.js", "types": "dist/index.d.ts",