From a36222e0e9ccfc3bd6ccabb2c99d5058b63c36a8 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jul 2024 00:07:03 -0700 Subject: [PATCH 1/5] docs: release v2.3.0 --- docs/reference/zmodel-language.md | 54 +++++++++++++++ docs/{sample-catalog.md => samples.md} | 2 +- .../part1/4-access-policy/4.1-model-level.md | 2 +- .../4.8-delegate-to-relation.md | 65 +++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) rename docs/{sample-catalog.md => samples.md} (98%) create mode 100644 docs/the-complete-guide/part1/4-access-policy/4.8-delegate-to-relation.md diff --git a/docs/reference/zmodel-language.md b/docs/reference/zmodel-language.md index 7ede8349..be609104 100644 --- a/docs/reference/zmodel-language.md +++ b/docs/reference/zmodel-language.md @@ -914,6 +914,60 @@ function future(): Any {} Gets the "post-update" state of an entity. Only valid when used in a "update" access policy. Read more about access policies [here](#access-policy). +##### check() + +```zmodel +function check(field: FieldReference, operation String?): Boolean {} +``` + +Checks if the current user can perform the given operation on the given field. + +_Params_ + +- `field`: The field to check access for. Must be a relation field. +- `operation`: The operation to check access for. Can be "read", "create", "update", or "delete". If the operation is not provided, it defaults the operation of the containing policy rule. + +_Example_ + +```zmodel +// delegating a single operation kind +model Post { + id Int @id + author User @relation(fields: [authorId], references: [id]) + authorId Int + + // delegate "read" check to the author, equivalent to + // @@allow('read', check(author)) + @@allow('read', check(author, 'read')) +} +``` + +```zmodel +// delegating all operations +model Post { + id Int @id + author User @relation(fields: [authorId], references: [id]) + authorId Int + + // delegate all access policies to the author, equivalent to: + // @@allow('read', check(author)) + // @@allow('create', check(author)) + // @@allow('update', check(author)) + // @@allow('delete', check(author)) + @@allow('all', check(author)) +} +``` + +```zmodel +// delegating field access control +model Post { + id Int @id + title String @allow('update', check(author)) + author User @relation(fields: [authorId], references: [id]) + authorId Int +} +``` + ##### contains() ```zmodel diff --git a/docs/sample-catalog.md b/docs/samples.md similarity index 98% rename from docs/sample-catalog.md rename to docs/samples.md index 7ffa0c3e..b9ab1a03 100644 --- a/docs/sample-catalog.md +++ b/docs/samples.md @@ -1,6 +1,6 @@ --- sidebar_position: 7 -sidebar_label: Sample Catalog +sidebar_label: Sample Projects --- # A Catalog of Sample Projects diff --git a/docs/the-complete-guide/part1/4-access-policy/4.1-model-level.md b/docs/the-complete-guide/part1/4-access-policy/4.1-model-level.md index 8adf085a..d9f197f5 100644 --- a/docs/the-complete-guide/part1/4-access-policy/4.1-model-level.md +++ b/docs/the-complete-guide/part1/4-access-policy/4.1-model-level.md @@ -69,7 +69,7 @@ Each of the CRUD operation types governs a set of Prisma Client methods, as foll - **read** - `findUnique`, `findUniqueOrThrow`, `findFirst`, `findFirstOrThrow`, `count`, `aggregate`, and `groupBy`. + `findMany`, `findUnique`, `findUniqueOrThrow`, `findFirst`, `findFirstOrThrow`, `count`, `aggregate`, and `groupBy`. The "read" operation also determines whether the value returned from `create`, `update` and `delete` method can be read. diff --git a/docs/the-complete-guide/part1/4-access-policy/4.8-delegate-to-relation.md b/docs/the-complete-guide/part1/4-access-policy/4.8-delegate-to-relation.md new file mode 100644 index 00000000..1c0fdde4 --- /dev/null +++ b/docs/the-complete-guide/part1/4-access-policy/4.8-delegate-to-relation.md @@ -0,0 +1,65 @@ +--- +sidebar_label: 4.8 Delegating Access Control To Relations +slug: delegate-to-relation +--- + +# Delegating Access Control To Relations + +ZenStack's access policies provide you with a powerful tool to model authorization in a intuitive and declarative way. However, as your application grows, you may find yourself repeating the same access policy patterns across multiple models. In this section, we'll review a typical type of duplication and show you how to avoid it by delegating to related models. + +### Parent-Child Duplication + +If your application has a hierarchy of models, you'll quite often notice that the child models repeat the access policies of their parent. This can be found in our Todo schema, where `List` and `Todo` models have a parent-child relationship: + +```zmodel +model List { + ... + + // require login + @@deny('all', auth() == null) + + // can be read by space members if not private + @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) + + // when create, owner must be set to current user, and user must be in the space + @@allow('create,update', owner == auth() && space.members?[user == auth()]) + + ... +} + +model Todo { + ... + + // require login + @@deny('all', auth() == null) + + // owner has full access + @@allow('all', list.owner == auth()) + + // space members have full access if the parent List is not private + @@allow('all', list.space.members?[user == auth()] && !list.private) +} +``` + +Although not a 100% match, the rules of `Todo` closely resemble those of `List`. How can we avoid such duplication and stay DRY? + +### Delegating Access Control to a Relation + +If we carefully inspect the policies, we'll find the rules of `Todo` can summarized into a single statement: + +> One has full access to a `Todo` entity if he can read its parent `List` entity. + +ZenStack provides a `check()` attribute function for you to delegate access control of the current model to a relation. Let's refactor the `Todo` model to use this feature 🛠️: + +```zmodel +model Todo { + ... + + // full access if the parent list is readable + @@allow('all', check(list, 'read')) +} +``` + +When authorizing an operation on a `Todo` (e.g., "update"), ZenStack will make the decision based on if the related `List` entity is readable. If you adjust the rules on `List` in the future, the `Todo` model automatically follows without needing any changes. + +The `check()` API can be used in both model-level and field-level policies. See the [API Reference](../../../reference/zmodel-language#check) for more details. \ No newline at end of file From f04e5c40899bd043185d2abc82879069b6a613c2 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jul 2024 00:09:06 -0700 Subject: [PATCH 2/5] fix build --- docs/welcome.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/welcome.md b/docs/welcome.md index eb99e7d0..11f220b4 100644 --- a/docs/welcome.md +++ b/docs/welcome.md @@ -43,7 +43,7 @@ If you're the kind of inpatient developer who wants to get your hand dirty ASAP, ## Other Resources - [Recipes](./category/recipes/) - Mini guides for completing specific tasks using ZenStack -- [Sample Catalog](./sample-catalog) - A catalog of sample projects using different frameworks +- [Samples](./samples) - A catalog of sample projects using different frameworks - [Reference](./category/reference/) - Detailed documentation for the DSL, CLI, APIs, and plugins - [FAQ](./faq) - Frequently asked questions From 41d217bd15b4117355404bc458190716b869a1cd Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jul 2024 00:25:21 -0700 Subject: [PATCH 3/5] update --- ...4.8-delegate-to-relation.md => 4.8-keep-policies-dry.md} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename docs/the-complete-guide/part1/4-access-policy/{4.8-delegate-to-relation.md => 4.8-keep-policies-dry.md} (95%) diff --git a/docs/the-complete-guide/part1/4-access-policy/4.8-delegate-to-relation.md b/docs/the-complete-guide/part1/4-access-policy/4.8-keep-policies-dry.md similarity index 95% rename from docs/the-complete-guide/part1/4-access-policy/4.8-delegate-to-relation.md rename to docs/the-complete-guide/part1/4-access-policy/4.8-keep-policies-dry.md index 1c0fdde4..b2944816 100644 --- a/docs/the-complete-guide/part1/4-access-policy/4.8-delegate-to-relation.md +++ b/docs/the-complete-guide/part1/4-access-policy/4.8-keep-policies-dry.md @@ -1,9 +1,9 @@ --- -sidebar_label: 4.8 Delegating Access Control To Relations -slug: delegate-to-relation +sidebar_label: 4.8 Keeping the Policies DRY +slug: keep-policies-dry --- -# Delegating Access Control To Relations +# Keeping the Policies DRY ZenStack's access policies provide you with a powerful tool to model authorization in a intuitive and declarative way. However, as your application grows, you may find yourself repeating the same access policy patterns across multiple models. In this section, we'll review a typical type of duplication and show you how to avoid it by delegating to related models. From a8b80fbe7a97facce2ba4994a4764169d3ae8de9 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 9 Jul 2024 10:35:12 -0700 Subject: [PATCH 4/5] add notes --- docs/reference/zmodel-language.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/zmodel-language.md b/docs/reference/zmodel-language.md index be609104..0affbb94 100644 --- a/docs/reference/zmodel-language.md +++ b/docs/reference/zmodel-language.md @@ -968,6 +968,10 @@ model Post { } ``` +:::info +The `check()` function only supports singular relation fields and cannot be used with "to-many" relations. We may add support for it in the future. +::: + ##### contains() ```zmodel From 4147ea98b268aedc1fffeb7097e1c7f0c11f4f36 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:43:50 -0700 Subject: [PATCH 5/5] update --- docs/reference/server-adapters/nestjs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/server-adapters/nestjs.mdx b/docs/reference/server-adapters/nestjs.mdx index b31305d9..8d0d8c4d 100644 --- a/docs/reference/server-adapters/nestjs.mdx +++ b/docs/reference/server-adapters/nestjs.mdx @@ -118,6 +118,6 @@ interface ZenStackModuleOptions { /** * A callback for getting an enhanced `PrismaClient`. */ - getEnhancedPrisma: () => unknown; + getEnhancedPrisma: (model?: string | symbol) => unknown; } ```