Skip to content

Commit

Permalink
fix: 🐛 only allow creation if object fits condition
Browse files Browse the repository at this point in the history
  • Loading branch information
dennemark committed Aug 14, 2024
1 parent 5234aef commit 7a6bd77
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 8 deletions.
6 changes: 4 additions & 2 deletions src/applyDataQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand Down Expand Up @@ -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 */
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
10 changes: 5 additions & 5 deletions test/applyDataQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 }] }] } })
})
Expand All @@ -116,7 +117,7 @@ describe('apply data query', () => {
id: 0
})
can('create', 'Post', {
id: 1
text: ''
})
can('update', 'Post', {
id: 2
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -295,6 +294,7 @@ describe('apply data query', () => {
}, 'create', 'User'))
.toThrow(`It's not allowed to "create" "text" on "Post"`)
})

})
})

Expand Down
170 changes: 170 additions & 0 deletions test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 7a6bd77

Please sign in to comment.