Skip to content

Commit 299da7f

Browse files
authored
fix(policy): reject access to undefined models (#499)
1 parent 5ac9e13 commit 299da7f

File tree

3 files changed

+71
-1
lines changed

3 files changed

+71
-1
lines changed

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
- [x] Inject "on conflict do update"
105105
- [x] `check` function
106106
- [ ] Custom functions
107-
- [ ] Accessing tables not in the schema
107+
- [x] Accessing tables not in the schema
108108
- [x] Migration
109109
- [ ] Databases
110110
- [x] SQLite

packages/plugins/policy/src/policy-handler.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
9090

9191
const { mutationModel } = this.getMutationModel(node);
9292

93+
this.tryRejectNonexistentModel(mutationModel);
94+
9395
// --- Pre mutation work ---
9496

9597
if (InsertQueryNode.is(node)) {
@@ -331,6 +333,8 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
331333
return super.transformJoin(node);
332334
}
333335

336+
this.tryRejectNonexistentModel(table.model);
337+
334338
// build a nested query with policy filter applied
335339
const filter = this.buildPolicyFilter(table.model, table.alias, 'read');
336340

@@ -872,6 +876,7 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
872876
const extractResult = this.extractTableName(table);
873877
if (extractResult) {
874878
const { model, alias } = extractResult;
879+
this.tryRejectNonexistentModel(model);
875880
const filter = this.buildPolicyFilter(model, alias, 'read');
876881
return acc ? conjunction(this.dialect, [acc, filter]) : filter;
877882
}
@@ -1011,5 +1016,11 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
10111016
return eb.and([aQuery, bQuery]).toOperationNode();
10121017
}
10131018

1019+
private tryRejectNonexistentModel(model: string) {
1020+
if (!QueryUtils.hasModel(this.client.$schema, model) && !this.isManyToManyJoinTable(model)) {
1021+
throw createRejectedByPolicyError(model, RejectedByPolicyReason.NO_ACCESS);
1022+
}
1023+
}
1024+
10141025
// #endregion
10151026
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('Policy tests for nonexistent models and fields', () => {
5+
it('rejects access to nonexistent model', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model Foo {
9+
id String @id @default(cuid())
10+
string String
11+
@@allow('all', true)
12+
}
13+
`,
14+
);
15+
const dbRaw = db.$unuseAll();
16+
17+
// create a Bar table
18+
await dbRaw.$executeRawUnsafe(
19+
`CREATE TABLE "Bar" ("id" TEXT PRIMARY KEY, "string" TEXT, "fooId" TEXT, FOREIGN KEY ("fooId") REFERENCES "Foo" ("id"));`,
20+
);
21+
22+
await dbRaw.$qb.insertInto('Foo').values({ id: '1', string: 'test' }).execute();
23+
await dbRaw.$qb.insertInto('Bar').values({ id: '1', string: 'test', fooId: '1' }).execute();
24+
25+
expect(db.bar).toBeUndefined();
26+
27+
// unknown relation
28+
await expect(db.foo.findFirst({ include: { bar: true } })).toBeRejectedByValidation();
29+
30+
// read
31+
await expect(db.$qb.selectFrom('Bar').selectAll().execute()).toBeRejectedByPolicy();
32+
33+
// join
34+
await expect(
35+
db.$qb.selectFrom('Foo').innerJoin('Bar', 'Bar.fooId', 'Foo.id').selectAll().execute(),
36+
).toBeRejectedByPolicy();
37+
38+
// create
39+
await expect(db.$qb.insertInto('Bar').values({ id: '1', string: 'test' }).execute()).toBeRejectedByPolicy();
40+
41+
// update
42+
await expect(
43+
db.$qb.updateTable('Bar').set({ string: 'updated' }).where('id', '=', '1').execute(),
44+
).toBeRejectedByPolicy();
45+
46+
// update with from
47+
await expect(
48+
db.$qb
49+
.updateTable('Foo')
50+
.set({ string: 'updated' })
51+
.from('Bar')
52+
.where('Bar.fooId', '=', 'Foo.id')
53+
.execute(),
54+
).toBeRejectedByPolicy();
55+
56+
// delete
57+
await expect(db.$qb.deleteFrom('Bar').where('id', '=', '1').execute()).toBeRejectedByPolicy();
58+
});
59+
});

0 commit comments

Comments
 (0)