diff --git a/package.json b/package.json index 8a23bbd4a..2f6cfb1d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index e52c47113..d0da06c48 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 5f2cbbecb..2a77579fc 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 2d7f9086c..151235f53 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 657460b4d..24af3683b 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index c6c5a3732..13bce2e36 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d8eae6123..93ecb9142 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index 5ed4eeea2..3da47b86a 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -5,9 +5,11 @@ import path from 'path'; import semver from 'semver'; import { PRISMA_MINIMUM_VERSION } from '../../constants'; import { AuthUser, DbClientContract } from '../../types'; +import { hasAllFields } from '../../validation'; import { getDefaultModelMeta } from '../model-meta'; import { makeProxy } from '../proxy'; import type { ModelMeta, PolicyDef, ZodSchemas } from '../types'; +import { getIdFields } from '../utils'; import { PolicyProxyHandler } from './handler'; /** @@ -70,6 +72,21 @@ export function withPolicy( const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(); const _zodSchemas = options?.zodSchemas ?? getDefaultZodSchemas(); + // validate user context + if (context?.user) { + const idFields = getIdFields(_modelMeta, 'User'); + if ( + !hasAllFields( + context.user, + idFields.map((f) => f.name) + ) + ) { + throw new Error( + `Invalid user context: must have valid ID field ${idFields.map((f) => `"${f.name}"`).join(', ')}` + ); + } + } + return makeProxy( prisma, _modelMeta, diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 4ba822f3c..e2d286da0 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -18,9 +18,13 @@ export function getModelFields(data: object) { * Gets id fields for the given model. */ export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) { - const fields = modelMeta.fields[lowerCaseFirst(model)]; + let fields = modelMeta.fields[lowerCaseFirst(model)]; if (!fields) { - throw new Error(`Unable to load fields for ${model}`); + if (throwIfNotFound) { + throw new Error(`Unable to load fields for ${model}`); + } else { + fields = {}; + } } const result = Object.values(fields).filter((f) => f.isId); if (result.length === 0 && throwIfNotFound) { diff --git a/packages/schema/package.json b/packages/schema/package.json index 8e511030a..a1f96545b 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts index 1fbba6800..54964edc1 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts @@ -25,6 +25,7 @@ import { getPrismaClientImportSpec, hasAttribute, hasValidationAttributes, + isForeignKeyField, PluginError, PluginOptions, resolved, @@ -274,6 +275,7 @@ export default class PolicyGenerator { // we can't check based on create input return false; } + if ( isDataModelField(expr.target.ref) && expr.target.ref.$container === model && @@ -284,6 +286,12 @@ export default class PolicyGenerator { // based on create input return false; } + + if (isDataModelField(expr.target.ref) && isForeignKeyField(expr.target.ref)) { + // reference to foreign key field + // we can't check based on create input + return false; + } } return true; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 5f6ab4fc8..e6d633928 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index cd599c5e9..e07ff912a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 465021a46..af678233f 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 396a09cb4..f620b5cd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,7 +121,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -192,7 +192,7 @@ importers: version: 2.0.3(react@18.2.0) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) + version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4) typescript: specifier: ^4.9.4 version: 4.9.4 @@ -5649,6 +5649,7 @@ packages: /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + requiresBuild: true dependencies: once: 1.4.0 @@ -6745,6 +6746,7 @@ packages: /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + requiresBuild: true /fs-extra@11.1.0: resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==} @@ -10774,7 +10776,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10805,7 +10807,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true @@ -10844,6 +10846,41 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.0.5(@babel/core@7.22.9)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.22.9 + bs-logger: 0.2.6 + esbuild: 0.18.13 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@18.0.0) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.3 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + /ts-morph@16.0.0: resolution: {integrity: sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==} dependencies: diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts index fb7e0c9b5..a2dfc1b86 100644 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ b/tests/integration/tests/enhancements/with-policy/auth.test.ts @@ -113,8 +113,7 @@ describe('With Policy: auth() test', () => { const authDb = withPolicy(); await expect(authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - const authDb1 = withPolicy({ id: null }); - await expect(authDb1.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).rejects.toThrow(); + expect(() => withPolicy({ id: null })).toThrow(/Invalid user context/); const authDb2 = withPolicy({ id: 'user1' }); await expect(authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); @@ -148,9 +147,6 @@ describe('With Policy: auth() test', () => { await expect(db.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - const authDb1 = withPolicy({ id: null }); - await expect(authDb1.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).rejects.toThrow(); - const authDb2 = withPolicy({ id: 'user1' }); await expect(authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); }); diff --git a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts index 7dd62364a..f48cdba45 100644 --- a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts +++ b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts @@ -124,7 +124,7 @@ describe('With Policy: multiple id fields', () => { await prisma.user.create({ data: { x: '1', y: '1' } }); await prisma.user.create({ data: { x: '1', y: '2' } }); - const anonDb = withPolicy({}); + const anonDb = withPolicy(); await expect( anonDb.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }) diff --git a/tests/integration/tests/regression/issues.test.ts b/tests/integration/tests/regression/issues.test.ts index cb46a8f46..b69d7b9fa 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/integration/tests/regression/issues.test.ts @@ -548,4 +548,55 @@ model Group { }) ).resolves.toHaveLength(2); }); + + it('issue 627', async () => { + const { prisma, withPolicy } = await loadSchema( + ` +model User { + id String @id @default(uuid()) +} + +abstract model BaseEntityWithTenant { + id String @id @default(uuid()) + + name String + tenant_id String + tenant tenant? @relation(fields: [tenant_id], references: [id]) + + @@allow('all', auth().id == tenant_id) +} + +model tenant { + id String @id @default(uuid()) + equipments Equipment[] +} + +model Equipment extends BaseEntityWithTenant { + a String +} +`, + { logPrismaQuery: true } + ); + + await prisma.tenant.create({ + data: { + id: 'tenant-1', + }, + }); + + const db = withPolicy({ id: 'tenant-1' }); + const r = await db.equipment.create({ + data: { + name: 'equipment-1', + tenant: { + connect: { + id: 'tenant-1', + }, + }, + a: 'a', + }, + }); + + console.log(r); + }); });