-
-
Notifications
You must be signed in to change notification settings - Fork 37
doc: v3 access policy and input validation #495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule v3-doc-orm-policy
added at
f4b864
Submodule v3-doc-orm-validation
added at
c86439
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| sidebar_position: 4 | ||
| --- | ||
|
|
||
| # Field-Level Policies 🚧 | ||
|
|
||
| Coming soon. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
versioned_docs/version-3.x/orm/access-control/post-update.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| --- | ||
| sidebar_position: 3 | ||
| --- | ||
|
|
||
| # Post-Update Rules | ||
|
|
||
| import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; | ||
|
|
||
| :::info | ||
|
|
||
| In ZenStack v2, post-update rules were implicitly defined with the "update" operation by using the `future()` function to refer to the post-update values. We found this approach to be unclean and error-prone. V3 made a breaking change to introduce a separate "post-update" operation. | ||
|
|
||
| ::: | ||
|
|
||
| ## Overview | ||
|
|
||
| Among the CRUD operations, "update" is a special one because it has a "pre" state and "post" state. The "update" policies we've seen in the previous parts refer to the "pre" state, meaning that if your polices refer to the model's fields, the fields are evaluated to their values before the update happens. | ||
|
|
||
| However, sometimes you want to express conditions that should hold after the update happens. For example, you may want to ensure that after an update, a post's `published` field cannot be set to true unless the current user is the author. Post-update policies are designed for such scenarios. | ||
|
|
||
| Writing post-update rules is essentially the same as writing regular "update" rules, except that fields will refer to their post-update values. You can use the built-in `before()` function to refer to the pre-update entity if needed. | ||
|
|
||
| Another key difference is that "post-update" operation is by default allowed. If you don't write any post-update rules, the update operation will succeed as long as it passes the "update" policies. However, if you have any post-update rules for a model, at least one `@@allow` rule must evaluate to true for the update operation to succeed. | ||
|
|
||
| ```zmodel | ||
| model Post { | ||
| id Int @id @default(autoincrement()) | ||
| title String | ||
| published Boolean @default(false) | ||
| author User @relation(fields: [authorId], references: [id]) | ||
| authorId Int | ||
|
|
||
| // only author can publish the post | ||
| @@deny('post-update', published == true && auth().id != authorId) | ||
|
|
||
| // prevent changing authorId | ||
| @@deny('post-update', before().authorId != authorId) | ||
| } | ||
| ``` | ||
|
|
||
| When post-update policies are violated, a `RejectedByPolicyError` is thrown. | ||
|
|
||
| ## Samples | ||
|
|
||
| <StackBlitzGithub repoPath="zenstackhq/v3-doc-orm-policy" openFile={['post-update/zenstack/schema.zmodel', 'post-update/main.ts']} startScript="post-update" /> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| --- | ||
| sidebar_position: 2 | ||
| --- | ||
|
|
||
| # Querying with Access Control | ||
|
|
||
| import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; | ||
|
|
||
| After defining access control policies in ZModel, it's time to enjoy their benefits. | ||
|
|
||
| ## Installing Runtime Plugin | ||
|
|
||
| Similar to the schema side, access control's runtime aspect is encapsulated in the `@zenstackhq/plugin-policy` package too, as a Runtime Plugin (more about this topic [later](../plugins/index.md)). You should install it on the raw ORM client to get a new client instance with access control enforcement. | ||
|
|
||
| ```ts | ||
| import { ZenStackClient } from '@zenstackhq/runtime'; | ||
| import { PolicyPlugin } from '@zenstackhq/plugin-policy'; | ||
|
|
||
| // create an unprotected, "raw" ORM client | ||
| const db = new ZenStackClient(...); | ||
|
|
||
| // install the policy plugin | ||
| const authDb = db.$use(new PolicyPlugin()); | ||
|
|
||
| // make queries with `authDb` to have access control enforced | ||
| ... | ||
| ``` | ||
|
|
||
| ## Setting Auth User | ||
|
|
||
| As mentioned in the previous part, you can use the `auth()` function in policy rules to refer to the current authenticated user. At runtime, you should use the `$setAuth()` API to provide such information. ZenStack itself is not an authentication library, so you need to determine how to achieve it based on your authentication mechanism. | ||
|
|
||
| In a web application, the typical pattern is to inspect the incoming request, extract and validate the user information from it, and then call `$setAuth()` to get an ORM client bound to that user. | ||
|
|
||
| ```ts | ||
| import { getSessionUser } from './auth'; // your auth helper | ||
| import { authDb } from './db'; // the client with policy plugin installed | ||
|
|
||
| async function handleRequest(req: Request) { | ||
| const user = await getSessionUser(req); | ||
|
|
||
| // create an user-bound client | ||
| const userDb = authDb.$setAuth(user); | ||
|
|
||
| // make queries with `userDb` to make user-bound queries | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| Without calling `$setAuth()`, the client works in anonymous mode, meaning that `auth()` in ZModel is evaluated to null. You can explicitly call `$setAuth(undefined)` to get an anonymous-bound client from a client that's previously bound to a user. | ||
|
|
||
| Use the `$auth` property to get the user info previously set by `$setAuth()`. | ||
|
|
||
| ## Making Queries | ||
|
|
||
| Access control policies are effective for both the ORM API and the query-builder API. To understand its behavior, the simplest mental model is to think that rows not satisfying the policies "don't exist". | ||
|
|
||
| ### ORM Queries | ||
|
|
||
| For the most part, the ORM query behavior is very intuitive: | ||
|
|
||
| - Read operations like `findMany`, `findUnique`, `count`, etc., only return/involve rows that meet the "read" policies. | ||
|
|
||
| - Mutation operations that affect multiple rows, like `updateMany` and `deleteMany`, only impact rows that meet the "update" or "delete" policies respectively. | ||
|
|
||
| - Mutation operations that affect a single, unique row, like `update` and `delete`, will throw an `NotFoundError` if the target row doesn't meet the "update" or "delete" policies respectively. | ||
|
|
||
| :::info | ||
| Why `NotFoundError` instead of `RejectedByPolicyError`? Because the rationale is rows that don't satisfy the policies "don't exist". | ||
| ::: | ||
|
|
||
| There are some complications when "read" and "write" policies affect the same query. It's ubiquitous because most mutation APIs involve reading the post-mutation entity to return to the caller. When the mutation succeeds but the post-mutation entity cannot be read, a `RejectedByPolicyError` is thrown, even though the mutation is persisted. | ||
|
|
||
| ```ts | ||
| // if Post#1 is updatable but the post-update read is not allowed, the | ||
| // update will be persisted first and then a `RejectedByPolicyError` | ||
| // will be thrown | ||
| await db.post.update({ | ||
| where: { id: 1 }, | ||
| data: { published: false }, | ||
| }); | ||
| ``` | ||
|
|
||
| :::info | ||
| Why throw an error instead of returning `null`? Because it'll compromise type-safety. The `create`, `update`, and `delete` APIs don't have a nullable return type. | ||
| ::: | ||
|
|
||
| ### Query-Builder Queries | ||
|
|
||
| The low-level Kysely query-builder API is also subject to access control enforcement. Its behavior is intuitive: | ||
|
|
||
| - Calling `$qb.selectFrom()` returns readable rows only. | ||
| - When you call `$qb.insertInto()`, `RejectedByPolicyError` will be thrown if the inserted row doesn't satisfy the "create" policies. Similar for `update` and `delete`. | ||
| - Calling `$qb.update()` and `$qb.delete()` only affects rows that satisfy the "update" and "delete" policies, respectively. | ||
| - When you join tables, the joined table will be filtered to readable rows only. | ||
| - When you use sub-queries, the sub-queries will be filtered to readable rows only. | ||
|
|
||
| ## Limitations | ||
|
|
||
| Here are some **IMPORTANT LIMITATIONS** about access control enforcement: | ||
|
|
||
| 1. Mutations caused by cascade deletes/updates and database triggers are entirely internal to the database, so ZenStack cannot enforce access control on them. | ||
| 2. Raw SQL queries executed via `$executeRaw()` and `$queryRaw()` are not subject to access control enforcement. | ||
| 3. Similarly, raw queries made with query-builder API using the `sql` tag are not subject to access control enforcement. | ||
|
|
||
| ## Samples | ||
|
|
||
| <StackBlitzGithub repoPath="zenstackhq/v3-doc-orm-policy" openFile={['basic/zenstack/schema.zmodel', 'basic/main.ts']} startScript="basic" /> | ||
|
|
||
| ## Implementation Notes | ||
|
|
||
| ZenStack v3's ORM is built on top of Kysely. Regardless of whether you use the ORM API or the query-builder one, queries are eventually transformed into Kysely's SQL AST and then compiled down to SQL and sent to the database for execution. The access control enforcement is implemented by transforming the AST and injecting proper filters. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.