|
| 1 | +import { loadSchema } from '@zenstackhq/testtools'; |
| 2 | + |
| 3 | +describe('issue 1574', () => { |
| 4 | + it('regression', async () => { |
| 5 | + const { enhance, prisma } = await loadSchema( |
| 6 | + ` |
| 7 | +model User { |
| 8 | + id String @id @default(cuid()) |
| 9 | + modelA ModelA[] |
| 10 | +} |
| 11 | +
|
| 12 | +// |
| 13 | +// ModelA has model-level access-all by owner, but read-all override for the name property |
| 14 | +// |
| 15 | +model ModelA { |
| 16 | + id String @id @default(cuid()) |
| 17 | +
|
| 18 | + owner User @relation(fields: [ownerId], references: [id]) |
| 19 | + ownerId String |
| 20 | +
|
| 21 | + name String @allow('read', true, true) |
| 22 | + prop2 String? |
| 23 | +
|
| 24 | + refsB ModelB[] |
| 25 | + refsC ModelC[] |
| 26 | +
|
| 27 | + @@allow('all', owner == auth()) |
| 28 | +} |
| 29 | +
|
| 30 | +// |
| 31 | +// ModelB and ModelC are both allow-all everyone. |
| 32 | +// They both have a reference to ModelA, but in ModelB that reference is optional. |
| 33 | +// |
| 34 | +model ModelB { |
| 35 | + id String @id @default(cuid()) |
| 36 | +
|
| 37 | + ref ModelA? @relation(fields: [refId], references: [id]) |
| 38 | + refId String? |
| 39 | +
|
| 40 | + @@allow('all', true) |
| 41 | +} |
| 42 | +model ModelC { |
| 43 | + id String @id @default(cuid()) |
| 44 | +
|
| 45 | + ref ModelA @relation(fields: [refId], references: [id]) |
| 46 | + refId String |
| 47 | +
|
| 48 | + @@allow('all', true) |
| 49 | +} |
| 50 | + `, |
| 51 | + { enhancements: ['policy'] } |
| 52 | + ); |
| 53 | + |
| 54 | + // create two users |
| 55 | + const user1 = await prisma.user.create({ data: { id: '1' } }); |
| 56 | + const user2 = await prisma.user.create({ data: { id: '2' } }); |
| 57 | + |
| 58 | + // create two db instances, enhanced for users 1 and 2 |
| 59 | + const db1 = enhance(user1); |
| 60 | + const db2 = enhance(user2); |
| 61 | + |
| 62 | + // create a ModelA owned by user1 |
| 63 | + const a = await db1.modelA.create({ data: { name: 'a', ownerId: user1.id } }); |
| 64 | + |
| 65 | + // create a ModelB and a ModelC with refs to ModelA |
| 66 | + const b = await db1.modelB.create({ data: { refId: a.id } }); |
| 67 | + const c = await db2.modelC.create({ data: { refId: a.id } }); |
| 68 | + |
| 69 | + // works: user1 should be able to read b as well as the entire referenced a |
| 70 | + const t1 = await db1.modelB.findFirst({ select: { ref: true } }); |
| 71 | + expect(t1.ref.name).toBeTruthy(); |
| 72 | + |
| 73 | + // works: user1 also should be able to read b as well as the name of the referenced a |
| 74 | + const t2 = await db1.modelB.findFirst({ select: { ref: { select: { name: true } } } }); |
| 75 | + expect(t2.ref.name).toBeTruthy(); |
| 76 | + |
| 77 | + // works: user2 also should be able to read b as well as the name of the referenced a |
| 78 | + const t3 = await db2.modelB.findFirst({ select: { ref: { select: { name: true } } } }); |
| 79 | + expect(t3.ref.name).toBeTruthy(); |
| 80 | + |
| 81 | + // works: but user2 should not be able to read b with the entire referenced a |
| 82 | + const t4 = await db2.modelB.findFirst({ select: { ref: true } }); |
| 83 | + expect(t4.ref).toBeFalsy(); |
| 84 | + |
| 85 | + // |
| 86 | + // The following are essentially the same tests, but with ModelC instead of ModelB |
| 87 | + // |
| 88 | + |
| 89 | + // works: user1 should be able to read c as well as the entire referenced a |
| 90 | + const t5 = await db1.modelC.findFirst({ select: { ref: true } }); |
| 91 | + expect(t5.ref.name).toBeTruthy(); |
| 92 | + |
| 93 | + // works: user1 also should be able to read c as well as the name of the referenced a |
| 94 | + const t6 = await db1.modelC.findFirst({ select: { ref: { select: { name: true } } } }); |
| 95 | + expect(t6.ref.name).toBeTruthy(); |
| 96 | + |
| 97 | + // works: user2 should not be able to read b along with the a reference. |
| 98 | + // In this case, the entire query returns null because of the required (but inaccessible) ref. |
| 99 | + await expect(db2.modelC.findFirst({ select: { ref: true } })).toResolveFalsy(); |
| 100 | + |
| 101 | + // works: if user2 queries c directly and gets the refId to a, it can get the a.name directly |
| 102 | + const t7 = await db2.modelC.findFirstOrThrow(); |
| 103 | + await expect(db2.modelA.findFirst({ select: { name: true }, where: { id: t7.refId } })).toResolveTruthy(); |
| 104 | + |
| 105 | + // fails: since the last query worked, we'd expect to be able to query c along with the name of the referenced a directly |
| 106 | + await expect(db2.modelC.findFirst({ select: { ref: { select: { name: true } } } })).toResolveTruthy(); |
| 107 | + }); |
| 108 | +}); |
0 commit comments