From 635b3a76e4bf7e469a56b20b7883617ee05c1d8c Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 7 May 2024 21:07:12 +0800 Subject: [PATCH 1/9] docs: permission checker documentation --- docs/guides/check-permission.md | 138 ++++++++++++++++++++++++++++++++ docs/reference/runtime-api.md | 2 +- 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 docs/guides/check-permission.md diff --git a/docs/guides/check-permission.md b/docs/guides/check-permission.md new file mode 100644 index 00000000..08c953bc --- /dev/null +++ b/docs/guides/check-permission.md @@ -0,0 +1,138 @@ +--- +description: Checking permissions without hitting the database. +sidebar_position: 13 +--- + +# Checking Permissions Without Hitting the Database (Preview) + +## Introduction + +ZenStack's access control features provide a protection layer around Prisma's CRUD operations and filter/deny access to the data automatically. However, there are cases where you simply want to check if an operation is permitted without actually executing it. For example, you might want to show or hide a button based on the user's permission. + +Of course, you can determine the permission by executing the operation to see if it's allowed (try reading data, or mutating inside a transaction then aborting). But this comes with the cost of increased database load and slower UI rendering. + +This guide introduces how to use ZenStack's `check` API to check permissions without accessing the database. The feature is in preview, and feedback is highly appreciated. + +## The Challenge and Solution + +This section provides background about the "permission checking" problem. Feel free to directly skip to the [Usage](#usage) section if you want to try it out right away. + +ZenStack's access policies are by design coupled with the data model, which implies that to check permission precisely, you'll have to evaluate it against the actual data. In reality, what you often need is an approximation, or in other words, a "weak" check. For example, you may want to check if the current user, given his role, can create an entity of a particular model, and if so, show the "create" button. You don't really want to guarantee that the user is allowed to create the entity with any data. What you care about is if he's allowed in some cases. + +With this in mind, "checking permission" is equivalent to answering the following question: + +> Assuming we can have arbitrary rows of data in the database, can the access policies for the given operation possibly evaluate to `TRUE` for the current user? + +The problem then becomes a [Boolean Satisfiability Problem](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem). We can treat model fields as "variables" and use a [SAT Solver](https://en.wikipedia.org/wiki/SAT_solver) to find a solution for those variables that satisfy the access policies. If a solution exists, then the permission is possible. + +Let's make the discussion more concrete by looking at an example: + +```zmodel +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + published Boolean @default(false) + + @@allow('read', published || auth().id == authorId || auth().role == 'ADMIN') +} +``` + +The "read" policy rule can be converted to a boolean formula like: + +``` +OR: + var(published) + `context.user.id` == var(authorId) + `context.user.role` == 'ADMIN' +``` + +:::info + +- The `context` object is the second argument you pass to the `enhance` API call. +- The `var(NAME)` symbol represents a named variable in a boolean formula. + +::: + +We can try solving for a solution for different cases: + +- Can an anonymous user read posts? + + Yes. Solution: `published = true` + +- Can `user#1` with "User" role read posts that are not published? + + :::info + The extra constraint imposed is `published == false`. + ::: + + Yes. Solution: `authorId = 1`, `published = false` + +- Can `user#1` with "User" role read unpublished posts of `user#2`? + + :::info + The extra constraint imposed is `published == false && authorId == 2`. + ::: + + No. No solution. + +- Can `user#1` with "ADMIN" role read unpublished posts of `user#2`? + + :::info + The extra constraint imposed is `published == false && authorId == 2`. + ::: + + Yes. The formula is constant `true` because of `context.user.role == 'ADMIN'`. + +## Usage + +To use the permission checking feature, first, enable the "generatePermissionChecker" preview flag for the "@core/enhancer" plugin in ZModel. + +```zmodel + +plugin enhancer { + provider = '@core/enhancer' + generatePermissionChecker = true +} + +``` + +Then, rerun `zenstack generate`, and the enhanced PrismaClient will have the extra `check` API on each model. + +```ts +const db = enhance(prisma, { user: getCurrentUser() }); +await canRead = await db.post.check({ operation: 'read' }); +``` + +The input also has an optional filter field for imposing additional constraints on model fields. + +```ts +await canRead = await db.post.check({ operation: 'read', filter: { published: true }}); +``` + +:::danger + +As explained in [the previous section](#the-challenge-and-solution), permission checking is an approximation and can be over-permissive. You SHOULD NOT trust it and circumvent the real access control mechanism (e.g., calling raw Prisma CRUD operations without further authorization checks). + +::: + +## Limitations + +ZenStack uses the [logic-solver](https://www.npmjs.com/package/logic-solver) package for SAT solving. The solver is lightweighted but only supports boolean and bits (non-negative integer) types. This resulted in the following limitations: + +- Only `Boolean`, `Int`, `String`, and enum types are supported. +- Functions (e.g., `startsWith`, `contains`, etc.) are not supported. +- Array fields are not supported. +- Relation fields are not supported. +- Collection predicates are not supported. + +You can still use the `check` API even if your access policies use these unsupported features. Boolean components containing unsupported features are ignored during SAT solving by being converted to free variables, which can be assigned either `true` or `false` in a solution. + +## Notes About Anonymous Context + +Access policy rules often use `auth()` and members of `auth()` (e.g., `auth().role`) in them. When a PrismaClient is enhanced in an anonymous context (calling `enhance` without context user object), neither `auth()` nor its members are unavailable. In such cases, the following evaluation rules apply: + +- `auth() == null` evaluates to `true`. +- `auth() != null` evaluates to `false`. +- Any other form of boolean component involving `auth()` or its members evaluates to `false`. diff --git a/docs/reference/runtime-api.md b/docs/reference/runtime-api.md index f0f15576..835b74a9 100644 --- a/docs/reference/runtime-api.md +++ b/docs/reference/runtime-api.md @@ -19,7 +19,7 @@ Creates an enhanced wrapper for a `PrismaClient`. The return value has the same ```ts function enhance( prisma: DbClient, - context?: WithPolicyContext, + context?: EnhancementContext, options?: EnhancementOptions ): DbClient; ``` From 8e883c3234c6a963f8ce12c890ec22401b054589 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 7 May 2024 22:59:25 +0800 Subject: [PATCH 2/9] update --- docs/guides/check-permission.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/check-permission.md b/docs/guides/check-permission.md index 08c953bc..72e7cc8c 100644 --- a/docs/guides/check-permission.md +++ b/docs/guides/check-permission.md @@ -105,15 +105,15 @@ const db = enhance(prisma, { user: getCurrentUser() }); await canRead = await db.post.check({ operation: 'read' }); ``` -The input also has an optional filter field for imposing additional constraints on model fields. +The input also has an optional where field for imposing additional constraints on model fields. ```ts -await canRead = await db.post.check({ operation: 'read', filter: { published: true }}); +await canRead = await db.post.check({ operation: 'read', where: { published: true }}); ``` :::danger -As explained in [the previous section](#the-challenge-and-solution), permission checking is an approximation and can be over-permissive. You SHOULD NOT trust it and circumvent the real access control mechanism (e.g., calling raw Prisma CRUD operations without further authorization checks). +As explained in [the previous section](#the-challenge-and-solution), permission checking is an approximation and can be over-permissive. You MUST NOT trust it and circumvent the real access control mechanism (e.g., calling raw Prisma CRUD operations without further authorization checks). ::: From a18b5d26120677d32291d841a0962729ae6c34f9 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 10 May 2024 11:53:16 +0800 Subject: [PATCH 3/9] addressing review comments --- docs/guides/check-permission.md | 148 +++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 50 deletions(-) diff --git a/docs/guides/check-permission.md b/docs/guides/check-permission.md index 72e7cc8c..19b50c59 100644 --- a/docs/guides/check-permission.md +++ b/docs/guides/check-permission.md @@ -7,17 +7,23 @@ sidebar_position: 13 ## Introduction -ZenStack's access control features provide a protection layer around Prisma's CRUD operations and filter/deny access to the data automatically. However, there are cases where you simply want to check if an operation is permitted without actually executing it. For example, you might want to show or hide a button based on the user's permission. +ZenStack's access policies provide a protection layer around Prisma's CRUD operations and filter/deny access to the data automatically. However, there are cases where you simply want to check if an operation is permitted without actually executing it. For example, you might want to show or hide a button based on the user's permission. -Of course, you can determine the permission by executing the operation to see if it's allowed (try reading data, or mutating inside a transaction then aborting). But this comes with the cost of increased database load and slower UI rendering. +Of course, you can determine the permission by executing the operation to see if it's allowed (try reading data, or mutating inside a transaction then aborting). But this comes with the cost of increased database load, slower UI rendering, and data pollution risks. This guide introduces how to use ZenStack's `check` API to check permissions without accessing the database. The feature is in preview, and feedback is highly appreciated. -## The Challenge and Solution +:::danger + +Permission checking is an approximation and can be over-permissive. You MUST NOT trust it and circumvent the real access control mechanism (e.g., calling raw Prisma CRUD operations without further authorization checks). + +::: + -This section provides background about the "permission checking" problem. Feel free to directly skip to the [Usage](#usage) section if you want to try it out right away. -ZenStack's access policies are by design coupled with the data model, which implies that to check permission precisely, you'll have to evaluate it against the actual data. In reality, what you often need is an approximation, or in other words, a "weak" check. For example, you may want to check if the current user, given his role, can create an entity of a particular model, and if so, show the "create" button. You don't really want to guarantee that the user is allowed to create the entity with any data. What you care about is if he's allowed in some cases. +## Understanding the Problem + +ZenStack's access policies are by design coupled with the data model, which implies that to check a permission precisely, you'll have to evaluate it against the actual data. In reality, what you often need is an approximation, or in other words, a "weak" check. For example, you may want to check if the current user, given his role, can read entities of a particular model, and if so, render the corresponding part of UI. You don't really want to guarantee that the user is allowed to read every row of that model. What you care about is if he's potentially allowed. With this in mind, "checking permission" is equivalent to answering the following question: @@ -35,91 +41,133 @@ model Post { authorId Int published Boolean @default(false) - @@allow('read', published || auth().id == authorId || auth().role == 'ADMIN') + @@allow('read', published || authorId == auth().id) } ``` The "read" policy rule can be converted to a boolean formula like: -``` -OR: - var(published) - `context.user.id` == var(authorId) - `context.user.role` == 'ADMIN' +```mermaid +flowchart LR + OR((OR)) --> A["[published] == true"] + OR((OR)) --> B["[authorId] == context.user.id"] ``` :::info - The `context` object is the second argument you pass to the `enhance` API call. -- The `var(NAME)` symbol represents a named variable in a boolean formula. +- A name wrapped with square brackets represents a named variable in a boolean formula. ::: -We can try solving for a solution for different cases: +To check if a user can read posts, we simply need to find a solution for the `published` and `authorId` variables that make the boolean formula evaluate to `TRUE`. -- Can an anonymous user read posts? - - Yes. Solution: `published = true` +## Using the `check` API -- Can `user#1` with "User" role read posts that are not published? - - :::info - The extra constraint imposed is `published == false`. - ::: +ZenStack adds a `check` API to every model in the enhanced PrismaClient. The feature is still in preview, so you need to explicitly opt-in by turning on the "generatePermissionChecker" flag on the "@core/enhancer" plugin in ZModel: - Yes. Solution: `authorId = 1`, `published = false` +```zmodel -- Can `user#1` with "User" role read unpublished posts of `user#2`? - - :::info - The extra constraint imposed is `published == false && authorId == 2`. - ::: +plugin enhancer { + provider = '@core/enhancer' + generatePermissionChecker = true +} - No. No solution. +``` -- Can `user#1` with "ADMIN" role read unpublished posts of `user#2`? - - :::info - The extra constraint imposed is `published == false && authorId == 2`. - ::: +Then, rerun `zenstack generate`, and the `check` API will be available on each model with the following signature (using the `Post` model as an example): - Yes. The formula is constant `true` because of `context.user.role == 'ADMIN'`. +```ts +type CheckArgs = { + /** + * The operation to check for + */ + operation: 'create' | 'read' | 'update' | 'delete'; + + /** + * The optional additional constraints to impose on the model fields + */ + where?: { id?: number; title?: string; published?: boolean; authorId?: number }; +} -## Usage +check(args: CheckArgs): Promise; +``` -To use the permission checking feature, first, enable the "generatePermissionChecker" preview flag for the "@core/enhancer" plugin in ZModel. +Let's see how to use it check `Post` readability for different use cases. Just to recap, the boolean formula for the "read" policy is: -```zmodel +```mermaid +flowchart LR + OR((OR)) --> A["[published] == true"] + OR((OR)) --> B["[authorId] == context.user.id"] +``` -plugin enhancer { - provider = '@core/enhancer' - generatePermissionChecker = true -} +### 1. Can an anonymous user read posts? + +The scenario is to determine if the `Posts` UI tab should be visible when the current user is not logged in. We can do the checking as follows: +```ts +const db = enhance(prisma); // enhance without a user context +await canRead = await db.post.check({ operation: 'read' }); ``` -Then, rerun `zenstack generate`, and the enhanced PrismaClient will have the extra `check` API on each model. +The result will be `true` with the following variable assignments: + +- `published -> true` +- `authorId -> 0` + +Note that the `authorId` variable can actually be any integer. + +### 2. Can an anonymous user read unpublished posts? + +The scenario is to determine if the `Drafts` UI tab should be visible when the current user is not logged in. ```ts -const db = enhance(prisma, { user: getCurrentUser() }); -await canRead = await db.post.check({ operation: 'read' }); +const db = enhance(prisma); // enhance without a user context +await canRead = await db.post.check({ operation: 'read', where: { published: false } }); ``` -The input also has an optional where field for imposing additional constraints on model fields. +We're now adding an additional constraint `published == false` that the solver needs to consider besides the original formula: + +```mermaid +flowchart LR + OR((OR)) --> A["[published] == true"] + OR((OR)) --> B["[authorId] == context.user.id"] + AND((AND)) --> C["[published] == false"] + AND((AND)) --> OR + style C stroke-dasharray: 5, 5 +``` + +The result will be `false` because there's no assignments of the `published` and `authorId` variables that satisfy the formula. Note that the `context.user.id` value is undefined thus cannot be equal to `authorId`. + +### 3. Can `user#1` read unpublished posts + +The scenario is to determine if the `Drafts` UI tab should be visible for a currently logged-in user. ```ts -await canRead = await db.post.check({ operation: 'read', where: { published: true }}); +const db = enhance(prisma, { user: { id: 1 } }); // enhance with user context +await canRead = await db.post.check({ operation: 'read', where: { published: false } }); ``` -:::danger +We're now providing a value `1` to `context.user.id`, and the formula becomes: -As explained in [the previous section](#the-challenge-and-solution), permission checking is an approximation and can be over-permissive. You MUST NOT trust it and circumvent the real access control mechanism (e.g., calling raw Prisma CRUD operations without further authorization checks). +```mermaid +flowchart LR + OR((OR)) --> A["[published] == true"] + OR((OR)) --> B["[authorId] == 1"] + AND((AND)) --> C["[published] == false"] + AND((AND)) --> OR + style C stroke-dasharray: 5, 5 +``` -::: + +The result will be `true` with the following variable assignments: + +- `published -> false` +- `authorId -> 1` ## Limitations -ZenStack uses the [logic-solver](https://www.npmjs.com/package/logic-solver) package for SAT solving. The solver is lightweighted but only supports boolean and bits (non-negative integer) types. This resulted in the following limitations: +ZenStack uses the [logic-solver](https://www.npmjs.com/package/logic-solver) package for SAT solving. The solver is lightweighted, but only supports boolean and bits (non-negative integer) types. This resulted in the following limitations: - Only `Boolean`, `Int`, `String`, and enum types are supported. - Functions (e.g., `startsWith`, `contains`, etc.) are not supported. From c0710b02737a537b5a066d300ee52e303d2a9528 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 10 May 2024 11:59:09 +0800 Subject: [PATCH 4/9] more changes --- docs/guides/check-permission.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides/check-permission.md b/docs/guides/check-permission.md index 19b50c59..72f33598 100644 --- a/docs/guides/check-permission.md +++ b/docs/guides/check-permission.md @@ -11,6 +11,8 @@ ZenStack's access policies provide a protection layer around Prisma's CRUD opera Of course, you can determine the permission by executing the operation to see if it's allowed (try reading data, or mutating inside a transaction then aborting). But this comes with the cost of increased database load, slower UI rendering, and data pollution risks. +Another choice is to implement permission checking logic directly inside your frontend code. However it'll be much nicer if the access policies in ZModel can be reused, so it stays as the single source of truth for access control. + This guide introduces how to use ZenStack's `check` API to check permissions without accessing the database. The feature is in preview, and feedback is highly appreciated. :::danger From ee0093fcb9021ff021d1a8ba6f90d608abc6676f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 10 May 2024 12:01:34 +0800 Subject: [PATCH 5/9] fix review comments --- docs/guides/check-permission.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/guides/check-permission.md b/docs/guides/check-permission.md index 72f33598..3208e5fd 100644 --- a/docs/guides/check-permission.md +++ b/docs/guides/check-permission.md @@ -21,11 +21,9 @@ Permission checking is an approximation and can be over-permissive. You MUST NOT ::: - - ## Understanding the Problem -ZenStack's access policies are by design coupled with the data model, which implies that to check a permission precisely, you'll have to evaluate it against the actual data. In reality, what you often need is an approximation, or in other words, a "weak" check. For example, you may want to check if the current user, given his role, can read entities of a particular model, and if so, render the corresponding part of UI. You don't really want to guarantee that the user is allowed to read every row of that model. What you care about is if he's potentially allowed. +ZenStack's access policies are by design coupled with the data model, which implies that to check permission precisely, you'll have to evaluate it against the actual data. In reality, what you often need is an approximation, or in other words, a "weak" check. For example, you may want to check if the current user, given his role, can read entities of a particular model, and if so, render the corresponding part of UI. You don't really want to guarantee that the user is allowed to read every row of that model. What you care about is if he's potentially allowed. With this in mind, "checking permission" is equivalent to answering the following question: @@ -66,7 +64,7 @@ To check if a user can read posts, we simply need to find a solution for the `pu ## Using the `check` API -ZenStack adds a `check` API to every model in the enhanced PrismaClient. The feature is still in preview, so you need to explicitly opt-in by turning on the "generatePermissionChecker" flag on the "@core/enhancer" plugin in ZModel: +ZenStack adds a `check` API to every model in the enhanced PrismaClient. The feature is still in preview, so you need to explicitly opt in by turning on the "generatePermissionChecker" flag on the "@core/enhancer" plugin in ZModel: ```zmodel @@ -95,7 +93,7 @@ type CheckArgs = { check(args: CheckArgs): Promise; ``` -Let's see how to use it check `Post` readability for different use cases. Just to recap, the boolean formula for the "read" policy is: +Let's see how to use it to check `Post` readability for different use cases. Just to recap, the boolean formula for the "read" policy is: ```mermaid flowchart LR @@ -139,7 +137,7 @@ flowchart LR style C stroke-dasharray: 5, 5 ``` -The result will be `false` because there's no assignments of the `published` and `authorId` variables that satisfy the formula. Note that the `context.user.id` value is undefined thus cannot be equal to `authorId`. +The result will be `false` because there are no assignments of the `published` and `authorId` variables that satisfy the formula. Note that the `context.user.id` value is undefined thus cannot be equal to `authorId`. ### 3. Can `user#1` read unpublished posts @@ -161,7 +159,6 @@ flowchart LR style C stroke-dasharray: 5, 5 ``` - The result will be `true` with the following variable assignments: - `published -> false` From dd9ac66bcad1f77f0bb7e88de43cfc7d8965052f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 10 May 2024 12:18:20 +0800 Subject: [PATCH 6/9] update --- docs/guides/check-permission.md | 47 ++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/guides/check-permission.md b/docs/guides/check-permission.md index 3208e5fd..6b4d9992 100644 --- a/docs/guides/check-permission.md +++ b/docs/guides/check-permission.md @@ -11,7 +11,7 @@ ZenStack's access policies provide a protection layer around Prisma's CRUD opera Of course, you can determine the permission by executing the operation to see if it's allowed (try reading data, or mutating inside a transaction then aborting). But this comes with the cost of increased database load, slower UI rendering, and data pollution risks. -Another choice is to implement permission checking logic directly inside your frontend code. However it'll be much nicer if the access policies in ZModel can be reused, so it stays as the single source of truth for access control. +Another choice is to implement permission checking logic directly inside your frontend code. However, it'll be much nicer if the access policies in ZModel can be reused, so it stays as the single source of truth for access control. This guide introduces how to use ZenStack's `check` API to check permissions without accessing the database. The feature is in preview, and feedback is highly appreciated. @@ -35,13 +35,13 @@ Let's make the discussion more concrete by looking at an example: ```zmodel model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int - published Boolean @default(false) + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int + published Boolean @default(false) - @@allow('read', published || authorId == auth().id) + @@allow('read', published || authorId == auth().id) } ``` @@ -79,15 +79,15 @@ Then, rerun `zenstack generate`, and the `check` API will be available on each m ```ts type CheckArgs = { - /** - * The operation to check for - */ - operation: 'create' | 'read' | 'update' | 'delete'; - - /** - * The optional additional constraints to impose on the model fields - */ - where?: { id?: number; title?: string; published?: boolean; authorId?: number }; + /** + * The operation to check for + */ + operation: 'create' | 'read' | 'update' | 'delete'; + + /** + * The optional additional constraints to impose on the model fields + */ + where?: { id?: number; title?: string; published?: boolean; authorId?: number }; } check(args: CheckArgs): Promise; @@ -164,6 +164,21 @@ The result will be `true` with the following variable assignments: - `published -> false` - `authorId -> 1` +## Server Adapters and Hooks + +The `check` API is also available in the [RPC API Handler](../reference/server-adapters/api-handlers/rpc) and can be used with all [server adapters](../category/server-adapters). + +The [@zenstackhq/tanstack-query](../reference/plugins/tanstack-query) and [@zenstackhq/swr](../reference/plugins/swr) plugins also generate `useCheck[Model]` hooks for checking permissions in the frontend. + +```ts +import { useCheckPost } from '~/lib/hooks'; + +const { data: canReadDrafts } = useCheckPost({ + operation: 'read', + where: { published: false } +}); +``` + ## Limitations ZenStack uses the [logic-solver](https://www.npmjs.com/package/logic-solver) package for SAT solving. The solver is lightweighted, but only supports boolean and bits (non-negative integer) types. This resulted in the following limitations: From dc13632ba22cb6fcac17ead0ec0625d423bd7bb6 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 10 May 2024 12:18:26 +0800 Subject: [PATCH 7/9] update --- docs/reference/server-adapters/api-handlers/rpc.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/server-adapters/api-handlers/rpc.mdx b/docs/reference/server-adapters/api-handlers/rpc.mdx index 4b7864df..ecfd1c96 100644 --- a/docs/reference/server-adapters/api-handlers/rpc.mdx +++ b/docs/reference/server-adapters/api-handlers/rpc.mdx @@ -236,6 +236,10 @@ The following part explains how the `meta` information is included for different _Http method:_ `DELETE` +- **[model]/check** + + _Http method:_ `GET` + ## HTTP Status Code and Error Responses ### Status code From 78bcc862832029111c17d9b3279c449bda317dc7 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 12 May 2024 21:40:41 +0800 Subject: [PATCH 8/9] more doc changes --- docs/guides/check-permission.md | 2 +- docs/guides/polymorphism.md | 2 +- docs/reference/error-handling.md | 2 +- docs/reference/plugins/_category_.yml | 2 +- docs/reference/plugins/swr.mdx | 30 ++++++++++ docs/reference/plugins/tanstack-query.mdx | 29 ++++++++++ docs/reference/prisma-client-ext.md | 57 +++++++++++++++++++ docs/reference/server-adapters/_category_.yml | 2 +- 8 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 docs/reference/prisma-client-ext.md diff --git a/docs/guides/check-permission.md b/docs/guides/check-permission.md index 6b4d9992..eddeb7c1 100644 --- a/docs/guides/check-permission.md +++ b/docs/guides/check-permission.md @@ -168,7 +168,7 @@ The result will be `true` with the following variable assignments: The `check` API is also available in the [RPC API Handler](../reference/server-adapters/api-handlers/rpc) and can be used with all [server adapters](../category/server-adapters). -The [@zenstackhq/tanstack-query](../reference/plugins/tanstack-query) and [@zenstackhq/swr](../reference/plugins/swr) plugins also generate `useCheck[Model]` hooks for checking permissions in the frontend. +The [@zenstackhq/tanstack-query](../reference/plugins/tanstack-query) and [@zenstackhq/swr](../reference/plugins/swr) plugins have been updated to generate `useCheck[Model]` hooks for checking permissions in the frontend. ```ts import { useCheckPost } from '~/lib/hooks'; diff --git a/docs/guides/polymorphism.md b/docs/guides/polymorphism.md index 6b5b5557..ad70febc 100644 --- a/docs/guides/polymorphism.md +++ b/docs/guides/polymorphism.md @@ -265,7 +265,7 @@ The main thing that ZenStack does internally is to translate between these two " - Inheriting from multiple `@delegate` models is not supported yet. -- You cannot access base fields when calling `count`, `aggregate`, and `groupBy`. The following query is not supported: +- You cannot access base fields when calling `count`, `aggregate`, and `groupBy` with a concrete model. The following query is not supported: ```ts // you can't access base fields (`published` here) when aggregating diff --git a/docs/reference/error-handling.md b/docs/reference/error-handling.md index b3de378a..f25da75d 100644 --- a/docs/reference/error-handling.md +++ b/docs/reference/error-handling.md @@ -1,6 +1,6 @@ --- description: Error handling -sidebar_position: 6 +sidebar_position: 7 --- # Error Handling diff --git a/docs/reference/plugins/_category_.yml b/docs/reference/plugins/_category_.yml index a2d8b377..29d8f6f3 100644 --- a/docs/reference/plugins/_category_.yml +++ b/docs/reference/plugins/_category_.yml @@ -1,4 +1,4 @@ -position: 4 +position: 5 label: Plugins collapsible: true collapsed: true diff --git a/docs/reference/plugins/swr.mdx b/docs/reference/plugins/swr.mdx index 512c9432..77aaa2ab 100644 --- a/docs/reference/plugins/swr.mdx +++ b/docs/reference/plugins/swr.mdx @@ -71,6 +71,36 @@ export default MyApp; | ------- | ------ | ------------------------------------------------------- | -------- | ------- | | output | String | Output directory (relative to the path of ZModel) | Yes | | +### Hooks Signature + +The generated hooks have the following signature convention. + +- **Query Hooks** + + ```ts + function use[Operation][Model](args?, options?); + ``` + + - `[Operation]`: query operation. E.g., "FindMany", "FindUnique", "Count". + - `[Model]`: the name of the model. E.g., "Post". + - `args`: Prisma query args. E.g., `{ where: { published: true } }`. + - `options`: swr options. + + The `data` field returned by the hooks call contains the Prisma query result. + +- **Mutation Hooks** + + ```ts + function use[Operation][Model](options?); + ``` + + - `[Operation]`: mutation operation. E.g., "Create", "UpdateMany". + - `[Model]`: the name of the model. E.g., "Post". + - `options`: swr options. + + The `trigger` function returned with the hooks call takes the corresponding Prisma mutation args as input. E.q., `{ data: { title: 'Post1' } }`. + + ### Example Here's a quick example with a blogging app. You can find a fully functional Todo app example [here](https://github.com/zenstackhq/sample-todo-nextjs). diff --git a/docs/reference/plugins/tanstack-query.mdx b/docs/reference/plugins/tanstack-query.mdx index d466cc2d..2aa265f0 100644 --- a/docs/reference/plugins/tanstack-query.mdx +++ b/docs/reference/plugins/tanstack-query.mdx @@ -36,6 +36,35 @@ npm install --save-dev @zenstackhq/tanstack-query | target | String | Target framework to generate for. Choose from "react", "vue", and "svelte". | Yes | | | version | String | Version of TanStack Query to generate for. Choose from "v4" and "v5". | No | v5 | +### Hooks Signature + +The generated hooks have the following signature convention. + +- **Query Hooks** + + ```ts + function use[Operation][Model](args?, options?); + ``` + + - `[Operation]`: query operation. E.g., "FindMany", "FindUnique", "Count". + - `[Model]`: the name of the model. E.g., "Post". + - `args`: Prisma query args. E.g., `{ where: { published: true } }`. + - `options`: tanstack-query options. + + The `data` field returned by the hooks call contains the Prisma query result. + +- **Mutation Hooks** + + ```ts + function use[Operation][Model](options?); + ``` + + - `[Operation]`: mutation operation. E.g., "Create", "UpdateMany". + - `[Model]`: the name of the model. E.g., "Post". + - `options`: TanStack-Query options. + + The `mutate` and `mutateAsync` functions returned with the hooks call take the corresponding Prisma mutation args as input. E.q., `{ data: { title: 'Post1' } }`. + ### Context Provider The generated hooks allow you to control their behavior by setting up context. The following options are available on the context: diff --git a/docs/reference/prisma-client-ext.md b/docs/reference/prisma-client-ext.md new file mode 100644 index 00000000..63358e8f --- /dev/null +++ b/docs/reference/prisma-client-ext.md @@ -0,0 +1,57 @@ +--- +description: APIs ZenStack adds to the PrismaClient +sidebar_position: 4 +sidebar_label: Added PrismaClient APIs +--- + +# Added PrismaClient APIs + +ZenStack's enhancement to PrismaClient not only alters its existing APIs' behavior, but also adds new APIs. + +### check + +#### Scope + +This API is added to each model in the PrismaClient. + +#### Description + +Checks if the current user is allowed to perform the specified operation on the model based on the access policies in ZModel. The check is done via pure logical inference and doesn't query the database. + +Please refer to [Checking Permissions Without Hitting the Database](../guides/check-permission) for more details. + +:::danger + +Permission checking is an approximation and can be over-permissive. You MUST NOT trust it and circumvent the real access control mechanism (e.g., calling raw Prisma CRUD operations without further authorization checks). + +::: + +#### Signature + +```ts +type CheckArgs = { + /** + * The operation to check for + */ + operation: 'create' | 'read' | 'update' | 'delete'; + + /** + * The optional additional constraints to impose on the model fields + */ + where?: { ... }; +} + +check(args: CheckArgs): Promise; +``` + +#### Example + +```ts +const db = enhance(prisma, { user: getCurrentUser() }); + +// check if the current user can read published posts +await canRead = await db.post.check({ + operation: 'read', + where: { published: true } +}); +``` diff --git a/docs/reference/server-adapters/_category_.yml b/docs/reference/server-adapters/_category_.yml index 21ccbbc0..a69eb540 100644 --- a/docs/reference/server-adapters/_category_.yml +++ b/docs/reference/server-adapters/_category_.yml @@ -1,4 +1,4 @@ -position: 5 +position: 6 label: Server Adapters collapsible: true collapsed: true From 1f46b8a66f4748853a87951064d544d0aace0d54 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 12 May 2024 21:48:34 +0800 Subject: [PATCH 9/9] update upgrade guide --- docs/upgrade.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/upgrade.md b/docs/upgrade.md index 358b3964..aad81beb 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -113,9 +113,23 @@ You can switch back to the old behavior in the extension settings (VSCode only). We've updated the `@zenstackhq/runtime` package to be compatible with Vercel Edge Runtime and Cloudflare Workers. See [this documentation](./guides/edge) for more details. -### 6. Permission Checker API 🚧 +### 6. Permission Checker API (Preview) -Coming soon. Please watch [this feature request](https://github.com/zenstackhq/zenstack/issues/242) for updates. +ZenStack's access policies prevent unauthorized users to query or mutate data. However, there are cases where you simply want to check if an operation is permitted without actually executing it. For example, you might want to show or hide a button based on the user's permission. + +The new permission checker API allows to check a user's permission without querying the database. + +```ts +const db = enhance(prisma, { user: getCurrentUser() }); + +// check if the current user can read published posts +await canRead = await db.post.check({ + operation: 'read', + where: { published: true } +}); +``` + +Please check [this guide](./guides/check-permission) for more details. ## Upgrading