diff --git a/TODO.md b/TODO.md index 0d4ad692..cd7e8eb8 100644 --- a/TODO.md +++ b/TODO.md @@ -101,6 +101,7 @@ - [ ] Short-circuit pre-create check for scalar-field only policies - [x] Inject "on conflict do update" - [x] `check` function + - [ ] Custom functions - [ ] Accessing tables not in the schema - [x] Migration - [ ] Databases diff --git a/packages/runtime/src/plugins/policy/expression-transformer.ts b/packages/runtime/src/plugins/policy/expression-transformer.ts index d78c2803..1eca04fa 100644 --- a/packages/runtime/src/plugins/policy/expression-transformer.ts +++ b/packages/runtime/src/plugins/policy/expression-transformer.ts @@ -523,7 +523,6 @@ export class ExpressionTransformer { }); if (currNode) { - invariant(SelectQueryNode.is(currNode), 'expected select query node'); currNode = { ...relation, selections: [ diff --git a/packages/runtime/test/policy/migrated/multi-id-fields.test.ts b/packages/runtime/test/policy/migrated/multi-id-fields.test.ts index 56941f03..9444fe20 100644 --- a/packages/runtime/test/policy/migrated/multi-id-fields.test.ts +++ b/packages/runtime/test/policy/migrated/multi-id-fields.test.ts @@ -57,8 +57,7 @@ describe('Policy tests multiple id fields', () => { ).toResolveTruthy(); }); - // TODO: `future()` support - it.skip('multi-id fields id update', async () => { + it('multi-id fields id update', async () => { const db = await createPolicyTestClient( ` model A { @@ -70,7 +69,8 @@ describe('Policy tests multiple id fields', () => { @@allow('read', true) @@allow('create', value > 0) - @@allow('update', value > 0 && future().value > 1) + @@allow('update', value > 0) + @@allow('post-update', value > 1) } model B { @@ -319,8 +319,7 @@ describe('Policy tests multiple id fields', () => { expect(await db.c.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 6 })); }); - // TODO: `future()` support - it.skip('multi-id fields nested id update', async () => { + it('multi-id fields nested id update', async () => { const db = await createPolicyTestClient( ` model A { @@ -333,7 +332,8 @@ describe('Policy tests multiple id fields', () => { @@allow('read', true) @@allow('create', value > 0) - @@allow('update', value > 0 && future().value > 1) + @@allow('update', value > 0) + @@allow('post-update', value > 1) } model B { @@ -369,7 +369,7 @@ describe('Policy tests multiple id fields', () => { upsert: { where: { x_y: { x: '2', y: 2 } }, update: { x: '3', y: 3, value: 0 }, - create: { x: '4', y: '4', value: 4 }, + create: { x: '4', y: 4, value: 4 }, }, }, }, @@ -384,7 +384,7 @@ describe('Policy tests multiple id fields', () => { upsert: { where: { x_y: { x: '2', y: 2 } }, update: { x: '3', y: 3, value: 3 }, - create: { x: '4', y: '4', value: 4 }, + create: { x: '4', y: 4, value: 4 }, }, }, }, diff --git a/packages/runtime/test/policy/migrated/nested-to-one.test.ts b/packages/runtime/test/policy/migrated/nested-to-one.test.ts index 5838cae8..432c8065 100644 --- a/packages/runtime/test/policy/migrated/nested-to-one.test.ts +++ b/packages/runtime/test/policy/migrated/nested-to-one.test.ts @@ -197,8 +197,7 @@ describe('With Policy:nested to-one', () => { ).toBeRejectedNotFound(); }); - // TODO: `future()` support - it.skip('nested update id tests', async () => { + it('nested update id tests', async () => { const db = await createPolicyTestClient( ` model M1 { @@ -216,7 +215,8 @@ describe('With Policy:nested to-one', () => { @@allow('read', true) @@allow('create', value > 0) - @@allow('update', value > 1 && future().value > 2) + @@allow('update', value > 1) + @@allow('post-update', value > 2) } `, ); diff --git a/packages/runtime/test/policy/migrated/petstore-sample.test.ts b/packages/runtime/test/policy/migrated/petstore-sample.test.ts index 99e5e8c7..2b210827 100644 --- a/packages/runtime/test/policy/migrated/petstore-sample.test.ts +++ b/packages/runtime/test/policy/migrated/petstore-sample.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from 'vitest'; import { createPolicyTestClient } from '../utils'; import { schema } from '../../schemas/petstore/schema'; -// TODO: `future()` support -describe.skip('Pet Store Policy Tests', () => { +describe('Pet Store Policy Tests', () => { it('crud', async () => { const petData = [ { diff --git a/packages/runtime/test/policy/migrated/todo-sample.test.ts b/packages/runtime/test/policy/migrated/todo-sample.test.ts index c81ac3f7..541ca69b 100644 --- a/packages/runtime/test/policy/migrated/todo-sample.test.ts +++ b/packages/runtime/test/policy/migrated/todo-sample.test.ts @@ -370,8 +370,7 @@ describe('Todo Policy Tests', () => { expect(r1.lists).toHaveLength(1); }); - // TODO: `future()` support - it.skip('post-update checks', async () => { + it('post-update checks', async () => { await createSpaceAndUsers(db.$unuseAll()); const user1Db = db.$setAuth({ id: user1.id }); diff --git a/packages/runtime/test/policy/migrated/toplevel-operations.test.ts b/packages/runtime/test/policy/migrated/toplevel-operations.test.ts index f545148c..f427c4ad 100644 --- a/packages/runtime/test/policy/migrated/toplevel-operations.test.ts +++ b/packages/runtime/test/policy/migrated/toplevel-operations.test.ts @@ -133,8 +133,7 @@ describe('Policy toplevel operations tests', () => { ).toBeTruthy(); }); - // TODO: `future()` support - it.skip('update id tests', async () => { + it('update id tests', async () => { const db = await createPolicyTestClient( ` model Model { @@ -143,7 +142,8 @@ describe('Policy toplevel operations tests', () => { @@allow('read', value > 1) @@allow('create', value > 0) - @@allow('update', value > 1 && future().value > 2) + @@allow('update', value > 1) + @@allow('post-update', value > 2) } `, ); @@ -164,7 +164,7 @@ describe('Policy toplevel operations tests', () => { value: 1, }, }), - ).toBeRejectedNotFound(); + ).toBeRejectedByPolicy(); // update success await expect( diff --git a/packages/runtime/test/policy/todo-sample.test.ts b/packages/runtime/test/policy/todo-sample.test.ts index 83c812b5..a53c7466 100644 --- a/packages/runtime/test/policy/todo-sample.test.ts +++ b/packages/runtime/test/policy/todo-sample.test.ts @@ -383,8 +383,7 @@ describe('todo sample tests', () => { expect(r1?.lists).toHaveLength(1); }); - // TODO: `future()` support - it.skip('works with post-update checks', async () => { + it('works with post-update checks', async () => { const anonDb = await createPolicyTestClient(schema); await createSpaceAndUsers(anonDb.$unuseAll()); diff --git a/packages/runtime/test/schemas/petstore/schema.ts b/packages/runtime/test/schemas/petstore/schema.ts index 59954144..a2eb7d67 100644 --- a/packages/runtime/test/schemas/petstore/schema.ts +++ b/packages/runtime/test/schemas/petstore/schema.ts @@ -92,6 +92,7 @@ export const schema = { }, attributes: [ { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("orderId"), "==", ExpressionUtils._null()), "||", ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("order"), ["user"]), "==", ExpressionUtils.call("auth"))) }] }, + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("update") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.call("auth"), "!=", ExpressionUtils._null()) }] }, { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("post-update") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.call("before"), ["name"]), "==", ExpressionUtils.field("name")), "&&", ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.call("before"), ["category"]), "==", ExpressionUtils.field("category"))), "&&", ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.call("before"), ["orderId"]), "==", ExpressionUtils._null())) }] } ], idFields: ["id"], diff --git a/packages/runtime/test/schemas/petstore/schema.zmodel b/packages/runtime/test/schemas/petstore/schema.zmodel index a7a0f53b..8809445c 100644 --- a/packages/runtime/test/schemas/petstore/schema.zmodel +++ b/packages/runtime/test/schemas/petstore/schema.zmodel @@ -35,6 +35,8 @@ model Pet { // unsold pets are readable to all; sold ones are readable to buyers only @@allow('read', orderId == null || order.user == auth()) + @@allow('update', auth() != null) + // only allow update to 'orderId' field if it's not set yet (unsold) @@allow('post-update', before().name == name && before().category == category && before().orderId == null) } diff --git a/packages/runtime/test/schemas/todo/schema.ts b/packages/runtime/test/schemas/todo/schema.ts index 14ef60d1..f0ae9c26 100644 --- a/packages/runtime/test/schemas/todo/schema.ts +++ b/packages/runtime/test/schemas/todo/schema.ts @@ -311,6 +311,7 @@ export const schema = { { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("ownerId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])), "||", ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("space"), ["members"]), "?", ExpressionUtils.binary(ExpressionUtils.field("userId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"]))), "&&", ExpressionUtils.unary("!", ExpressionUtils.field("private")))) }] }, { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("create") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("ownerId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])), "&&", ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("space"), ["members"]), "?", ExpressionUtils.binary(ExpressionUtils.field("userId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])))) }] }, { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("update") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("ownerId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])), "&&", ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("space"), ["members"]), "?", ExpressionUtils.binary(ExpressionUtils.field("userId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])))) }] }, + { name: "@@deny", args: [{ name: "operation", value: ExpressionUtils.literal("post-update") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.call("before"), ["ownerId"]), "!=", ExpressionUtils.field("ownerId")) }] }, { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("delete") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.field("ownerId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])) }] } ], idFields: ["id"], @@ -380,7 +381,8 @@ export const schema = { attributes: [ { name: "@@deny", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.call("auth"), "==", ExpressionUtils._null()) }] }, { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("list"), ["ownerId"]), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])) }] }, - { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("list"), ["space", "members"]), "?", ExpressionUtils.binary(ExpressionUtils.field("userId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"]))), "&&", ExpressionUtils.unary("!", ExpressionUtils.member(ExpressionUtils.field("list"), ["private"]))) }] } + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("list"), ["space", "members"]), "?", ExpressionUtils.binary(ExpressionUtils.field("userId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"]))), "&&", ExpressionUtils.unary("!", ExpressionUtils.member(ExpressionUtils.field("list"), ["private"]))) }] }, + { name: "@@deny", args: [{ name: "operation", value: ExpressionUtils.literal("post-update") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.call("before"), ["ownerId"]), "!=", ExpressionUtils.field("ownerId")) }] } ], idFields: ["id"], uniqueFields: { diff --git a/packages/runtime/test/schemas/todo/todo.zmodel b/packages/runtime/test/schemas/todo/todo.zmodel index d91ed34a..faeaa660 100644 --- a/packages/runtime/test/schemas/todo/todo.zmodel +++ b/packages/runtime/test/schemas/todo/todo.zmodel @@ -117,10 +117,9 @@ model List { // when create, owner must be set to current user, and user must be in the space // update is not allowed to change owner - @@allow('update', ownerId == auth().id && space.members?[userId == auth().id] - // TODO: future() support - // && future().ownerId == ownerId - ) + @@allow('update', ownerId == auth().id && space.members?[userId == auth().id]) + + @@deny('post-update', before().ownerId != ownerId) // can be deleted by owner @@allow('delete', ownerId == auth().id) @@ -147,7 +146,6 @@ model Todo { @@allow('all', list.ownerId == auth().id) @@allow('all', list.space.members?[userId == auth().id] && !list.private) - // TODO: future() support - // // update is not allowed to change owner - // @@deny('update', future().owner != owner) + // update is not allowed to change owner + @@deny('post-update', before().ownerId != ownerId) }