diff --git a/src/applyDataQuery.ts b/src/applyDataQuery.ts index 3b3fdbf..67655fd 100644 --- a/src/applyDataQuery.ts +++ b/src/applyDataQuery.ts @@ -26,7 +26,9 @@ export function applyDataQuery( action: string, model: string ) { - const permittedFields = getPermittedFields(abilities, action, model) + // on creation we either have { data/create: input } or { ...input } and we check if fields are permitted. + const obj = action === 'update' ? undefined : 'data' in args ? args.data : 'create' in args ? args.create : args + const permittedFields = getPermittedFields(abilities, action, model, obj) const accessibleQuery = accessibleBy(abilities, action)[model as Prisma.ModelName] const mutationArgs: any[] = [] @@ -54,7 +56,6 @@ export function applyDataQuery( if (mutationArgs.length === 0) { mutationArgs.push(argsEntry) } - }) /** now we go trough all mutation args and throw error if they have not permitted fields or continue in nested mutations */ @@ -71,6 +72,7 @@ export function applyDataQuery( return field } }) + queriedFields.forEach((field) => { const relationModel = relationFieldsByModel[model][field] // omit relation models also through i.e. stat diff --git a/src/helpers.ts b/src/helpers.ts index 97e6d20..1815bab 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -2,7 +2,7 @@ import { AbilityTuple, PureAbility, Subject, subject } from '@casl/ability' import { permittedFieldsOf } from '@casl/ability/extra' import { prismaQuery, PrismaQuery } from '@casl/prisma' import { Prisma } from '@prisma/client' -import { DMMF } from '@prisma/generator-helper' +import type { DMMF } from '@prisma/generator-helper' type DefaultCaslAction = "create" | "read" | "update" | "delete" diff --git a/test/applyDataQuery.test.ts b/test/applyDataQuery.test.ts index 9bbac7b..1b55dbd 100644 --- a/test/applyDataQuery.test.ts +++ b/test/applyDataQuery.test.ts @@ -19,7 +19,7 @@ describe('apply data query', () => { expect(() => applyDataQuery(build(), { data: { authorId: 0 }, where: { id: 0 } }, 'update', 'Post')).toThrow(`It's not allowed to run "update" on "User"`) }) ;['update', 'create'].map((mutation) => { - describe('mutation', () => { + describe(mutation, () => { it('adds where clause to query', () => { const { can, build } = abilityBuilder() @@ -96,6 +96,7 @@ describe('apply data query', () => { can('update', 'Post', { id: 1 }) + const result = applyDataQuery(build(), { data: { id: 1, posts: { connect: [{ id: 0 }] } }, where: { id: 0 } }, 'update', 'User') expect(result).toEqual({ data: { id: 1, posts: { connect: [{ id: 0, AND: [{ OR: [{ id: 1 }] }] }] } }, where: { id: 0, AND: [{ OR: [{ id: 0 }] }] } }) }) @@ -116,7 +117,7 @@ describe('apply data query', () => { id: 0 }) can('create', 'Post', { - id: 1 + text: '' }) can('update', 'Post', { id: 2 @@ -146,9 +147,7 @@ describe('apply data query', () => { can('update', 'User', { id: 0 }) - can('create', 'Post', { - id: 1 - }) + can('create', 'Post') can('update', 'Post', { id: 2 }) @@ -295,6 +294,7 @@ describe('apply data query', () => { }, 'create', 'User')) .toThrow(`It's not allowed to "create" "text" on "Post"`) }) + }) }) diff --git a/test/extension.test.ts b/test/extension.test.ts index 5480860..d157b0b 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -837,6 +837,88 @@ describe('prisma extension casl', () => { }) expect(result).toEqual({ email: 'new', posts: [{ id: 0, text: '1' }, { id: 3, text: '' }] }) }) + it('can do nested updates with conditions', async () => { + function builderFactory() { + const builder = abilityBuilder() + const { can, cannot } = builder + + can('read', 'User', 'email') + can('update', 'User') + can('update', 'Post', { + id: 0 + }) + can('read', 'Post') + return builder + } + const client = seedClient.$extends( + useCaslAbilities(builderFactory) + ) + const result = await client.user.update({ + data: { + email: 'new', + posts: { + update: { + data: { + text: '1' + }, + where: { + id: 0 + } + } + } + }, + where: { + id: 0 + }, + include: { + posts: { + select: { id: true, text: true } + } + } + }) + expect(result).toEqual({ email: 'new', posts: [{ id: 0, text: '1' }, { id: 3, text: '' }] }) + }) + it('cannot do nested updates with failing conditions', async () => { + function builderFactory() { + const builder = abilityBuilder() + const { can, cannot } = builder + + can('read', 'User', 'email') + can('update', 'User') + can('update', 'Post', { + id: 1 + }) + can('read', 'Post') + return builder + } + const client = seedClient.$extends( + useCaslAbilities(builderFactory) + ) + await expect(client.user.update({ + data: { + email: 'new', + posts: { + update: { + data: { + text: '1' + }, + where: { + id: 0 + } + } + } + }, + where: { + id: 0 + }, + include: { + posts: { + select: { id: true, text: true } + } + } + })).rejects.toThrow() + + }) it('cannot do nested updates if no ability exists', async () => { function builderFactory() { const builder = abilityBuilder() @@ -1060,6 +1142,94 @@ describe('prisma extension casl', () => { expect(await seedClient.user.count()).toBe(1) }) }) + describe('create', () => { + it('cant do nested create with conditions', async () => { + function builderFactory() { + const builder = abilityBuilder() + const { can, cannot } = builder + + can('read', 'User', 'email') + can('create', 'User') + can('update', 'Thread') + can('create', 'Post', { + text: '1' + }) + can('read', 'Post') + return builder + } + const client = seedClient.$extends( + useCaslAbilities(builderFactory) + ) + const result = await client.user.create({ + data: { + email: 'new', + + posts: { + create: { + threadId: 0, + text: '1' + } + } + } + }) + expect(result).toEqual({ email: 'new' }) + }) + it('cannot do nested create with conditions', async () => { + function builderFactory() { + const builder = abilityBuilder() + const { can, cannot } = builder + + can('read', 'User', 'email') + can('create', 'User') + can('update', 'Thread') + can('create', 'Post', { + author: { + is: { + email: 'new' + } + } + }) + can('read', 'Post') + return builder + } + const client = seedClient.$extends( + useCaslAbilities(builderFactory) + ) + await expect(client.user.create({ + data: { + email: 'new', + + posts: { + create: { + threadId: 0, + text: '1' + } + } + } + })).rejects.toThrow() + }) + it('cannot do create with failing conditions', async () => { + function builderFactory() { + const builder = abilityBuilder() + const { can, cannot } = builder + + can('read', 'User', 'email') + can('create', 'User', { + email: 'old' + }) + return builder + } + const client = seedClient.$extends( + useCaslAbilities(builderFactory) + ) + await expect(client.user.create({ + data: { + email: 'new', + } + })).rejects.toThrow() + }) + + }) describe('fluent api queries', () => { it('can do chained queries if abilities exist', async () => { function builderFactory() {