From 83f45e0713ced33b874d2e6737e980adfaaa6ab0 Mon Sep 17 00:00:00 2001 From: dennemark Date: Mon, 30 Sep 2024 17:39:08 +0200 Subject: [PATCH] fix: :bug: create correct extended instance of getAbilities --- src/index.ts | 216 +++++++++++++++++++++-------------------- test/extension.test.ts | 26 ++++- 2 files changed, 135 insertions(+), 107 deletions(-) diff --git a/src/index.ts b/src/index.ts index ffa8709..824ac15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,108 @@ export function useCaslAbilities(getAbilityFactory: () => AbilityBuilder { - let getAbilities = () => getAbilityFactory() + const allOperations = (getAbilities: () => AbilityBuilder>) => ({ + async $allOperations({ args, query, model, operation, ...rest }: { args: any, query: any, model: any, operation: any }) { + const op = operation === 'createMany' ? 'createManyAndReturn' : operation + const transaction = (rest as any).__internalParams.transaction + const debug = (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && args.debugCasl + delete args.debugCasl + const perf = debug ? performance : undefined + const logger = debug ? console : undefined + perf?.clearMeasures('prisma-casl-extension-Overall') + perf?.clearMeasures('prisma-casl-extension-Create Abilities') + perf?.clearMeasures('prisma-casl-extension-Create Casl Query') + perf?.clearMeasures('prisma-casl-extension-Finish Query') + perf?.clearMeasures('prisma-casl-extension-Filtering Results') + perf?.clearMarks('prisma-casl-extension-0') + perf?.clearMarks('prisma-casl-extension-1') + perf?.clearMarks('prisma-casl-extension-2') + perf?.clearMarks('prisma-casl-extension-3') + perf?.clearMarks('prisma-casl-extension-4') + + if (!(op in caslOperationDict)) { + return query(args) + } + + + perf?.mark('prisma-casl-extension-0') + const abilities = transaction?.abilities ?? getAbilities().build() + if (transaction) { + transaction.abilities = abilities + } + perf?.mark('prisma-casl-extension-1') + + + 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: any) => { + + perf?.mark('prisma-casl-extension-3') + const fluentModel = getFluentModel(model, rest) + + if (fluentModel !== model && caslQuery.mask) { + // on fluent models we need to take mask of the relation + const relation = Object.entries(relationFieldsByModel[model]).find(([k, v]) => v.type === fluentModel)?.[0] + caslQuery.mask = relation && relation in caslQuery.mask ? caslQuery.mask[relation] : {} + } + const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, permissionField) + + if (perf) { + perf.mark('prisma-casl-extension-4') + logger?.log( + [perf.measure('prisma-casl-extension-Overall', 'prisma-casl-extension-0', 'prisma-casl-extension-4'), + perf.measure('prisma-casl-extension-Create Abilities', 'prisma-casl-extension-0', 'prisma-casl-extension-1'), + perf.measure('prisma-casl-extension-Create Casl Query', 'prisma-casl-extension-1', 'prisma-casl-extension-2'), + perf.measure('prisma-casl-extension-Finish Query', 'prisma-casl-extension-2', 'prisma-casl-extension-3'), + perf.measure('prisma-casl-extension-Filtering Results', 'prisma-casl-extension-3', 'prisma-casl-extension-4') + ].map((measure) => { + return `${measure.name.replace('prisma-casl-extension-', '')}: ${measure.duration}` + }) + ) + } + + return operation === 'createMany' ? { count: filteredResult.length } : filteredResult + } + const operationAbility = caslOperationDict[operation as PrismaCaslOperation] + /** + * on update or create we need to create a transaction + * since there can be errors if newly created db entries + * are not permitted by abilities + * + * for reads and deletes we skip the transaction + */ + if (operationAbility.action === 'update' || operationAbility.action === 'create') { + if (transaction) { + if (transaction.kind === 'itx') { + const transactionClient = (client as any)._createItxClient(transaction) + return transactionClient[model][op](caslQuery.args).then(cleanupResults) + } else if (transaction.kind === 'batch') { + //@ts-ignore + throw new Error('Sequential transactions are not supported in prisma-extension-casl.') + // const extendedRequest = request.then(cleanupResults) + // extendedRequest.requestTransaction = request.requestTransaction + //@ts-ignore + // return client._createPrismaPromise(new Promise((resolve, reject) => { + // query(caslQuery.args).then(cleanupResults).then((result: any) => resolve(result)).catch(((e: any) => reject(e))) + // }) + } + } else { + + return client.$transaction(async (tx) => { + //@ts-ignore + return tx[model][op](caslQuery.args).then(cleanupResults) + }) + } + } else { + return query(caslQuery.args).then(cleanupResults) + } + } + }) return client.$extends({ name: "prisma-extension-casl", client: { @@ -35,116 +136,19 @@ export function useCaslAbilities(getAbilityFactory: () => AbilityBuilder>) => AbilityBuilder>) { - const ctx = Prisma.getExtensionContext(this) // alter the getAblities function shortly - getAbilities = () => extendFactory(getAbilityFactory()) - return ctx as typeof client + return client.$extends({ + query: { + $allModels: { + ...allOperations(() => extendFactory(getAbilityFactory())) + } + } + }) } }, query: { $allModels: { - async $allOperations({ args, query, model, operation, ...rest }: { args: any, query: any, model: any, operation: any }) { - const op = operation === 'createMany' ? 'createManyAndReturn' : operation - const transaction = (rest as any).__internalParams.transaction - const debug = (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && args.debugCasl - delete args.debugCasl - const perf = debug ? performance : undefined - const logger = debug ? console : undefined - perf?.clearMeasures('prisma-casl-extension-Overall') - perf?.clearMeasures('prisma-casl-extension-Create Abilities') - perf?.clearMeasures('prisma-casl-extension-Create Casl Query') - perf?.clearMeasures('prisma-casl-extension-Finish Query') - perf?.clearMeasures('prisma-casl-extension-Filtering Results') - perf?.clearMarks('prisma-casl-extension-0') - perf?.clearMarks('prisma-casl-extension-1') - perf?.clearMarks('prisma-casl-extension-2') - perf?.clearMarks('prisma-casl-extension-3') - perf?.clearMarks('prisma-casl-extension-4') - - if (!(op in caslOperationDict)) { - return query(args) - } - - - perf?.mark('prisma-casl-extension-0') - const abilities = transaction?.abilities ?? getAbilities().build() - if (transaction) { - transaction.abilities = abilities - } - // reset alteration of getAblities function - getAbilities = () => getAbilityFactory() - perf?.mark('prisma-casl-extension-1') - - - 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: any) => { - - perf?.mark('prisma-casl-extension-3') - const fluentModel = getFluentModel(model, rest) - - if (fluentModel !== model && caslQuery.mask) { - // on fluent models we need to take mask of the relation - const relation = Object.entries(relationFieldsByModel[model]).find(([k, v]) => v.type === fluentModel)?.[0] - caslQuery.mask = relation && relation in caslQuery.mask ? caslQuery.mask[relation] : {} - } - const filteredResult = filterQueryResults(result, caslQuery.mask, caslQuery.creationTree, abilities, fluentModel, permissionField) - - if (perf) { - perf.mark('prisma-casl-extension-4') - logger?.log( - [perf.measure('prisma-casl-extension-Overall', 'prisma-casl-extension-0', 'prisma-casl-extension-4'), - perf.measure('prisma-casl-extension-Create Abilities', 'prisma-casl-extension-0', 'prisma-casl-extension-1'), - perf.measure('prisma-casl-extension-Create Casl Query', 'prisma-casl-extension-1', 'prisma-casl-extension-2'), - perf.measure('prisma-casl-extension-Finish Query', 'prisma-casl-extension-2', 'prisma-casl-extension-3'), - perf.measure('prisma-casl-extension-Filtering Results', 'prisma-casl-extension-3', 'prisma-casl-extension-4') - ].map((measure) => { - return `${measure.name.replace('prisma-casl-extension-', '')}: ${measure.duration}` - }) - ) - } - - return operation === 'createMany' ? { count: filteredResult.length } : filteredResult - } - const operationAbility = caslOperationDict[operation as PrismaCaslOperation] - /** - * on update or create we need to create a transaction - * since there can be errors if newly created db entries - * are not permitted by abilities - * - * for reads and deletes we skip the transaction - */ - if (operationAbility.action === 'update' || operationAbility.action === 'create') { - if (transaction) { - if (transaction.kind === 'itx') { - const transactionClient = (client as any)._createItxClient(transaction) - return transactionClient[model][op](caslQuery.args).then(cleanupResults) - } else if (transaction.kind === 'batch') { - //@ts-ignore - throw new Error('Sequential transactions are not supported in prisma-extension-casl.') - // const extendedRequest = request.then(cleanupResults) - // extendedRequest.requestTransaction = request.requestTransaction - //@ts-ignore - // return client._createPrismaPromise(new Promise((resolve, reject) => { - // query(caslQuery.args).then(cleanupResults).then((result: any) => resolve(result)).catch(((e: any) => reject(e))) - // }) - } - } else { - - return client.$transaction(async (tx) => { - //@ts-ignore - return tx[model][op](caslQuery.args).then(cleanupResults) - }) - } - } else { - return query(caslQuery.args).then(cleanupResults) - } - }, + ...allOperations(getAbilityFactory) }, } }) diff --git a/test/extension.test.ts b/test/extension.test.ts index 2dbcafc..6aef2bf 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -1583,7 +1583,31 @@ describe('prisma extension casl', () => { await expect(client.user.findUnique({ where: { id: 0 } }).posts()).rejects.toThrow() }) - + it('can do chained queries with local abilities', async () => { + function builderFactory() { + const builder = abilityBuilder() + const { can, cannot } = builder + can('read', 'User') + return builder + } + const client = seedClient.$extends( + useCaslAbilities(builderFactory) + ) + // await expect(await client.user.findUnique({ where: { id: 0 } }).posts()).rejects.toThrow() + + const result = await client.$casl((abilities) => { + abilities.can('read', 'Post') + return abilities + }).user.findUnique({ where: { id: 0 } }).posts() + expect(result).toEqual([{ authorId: 0, text: '', id: 0, threadId: 0 }, + { + authorId: 0, + id: 3, + text: '', + threadId: 2, + }, + ]) + }) }) describe('store permissions', () => { it('has permissions on custom prop on chained queries', async () => {