zenstackhq / zenstack

Fullstack TypeScript toolkit that enhances Prisma ORM with flexible Authorization layer for RBAC/ABAC/PBAC/ReBAC, offering auto-generated type-safe APIs and frontend hooks.
https://zenstack.dev
MIT License
2.1k stars 88 forks source link

[Feature Request] Allow custom messages in policies #1723

Open LilaRest opened 1 month ago

LilaRest commented 1 month ago

Is your feature request related to a problem? Please describe. Actually, once a policy is violated, ZenStack returns the following error object, which is not super descriptive for consumers. That becomes an even more important problem if the ZenStack-generated API is to be exposed to other developers as a public API or in an SDK.

denied by policy: user entities failed 'create' check
Code: P2004
Meta: { reason: 'ACCESS_POLICY_VIOLATION' }

Describe the solution you'd like. It'd be gorgeous to allow a third message parameter into @@allow and @@deny clauses. Something like that:

@@allow("create", organization.memberships?[actor == auth() && role == "Admin"], "Only admins can create memberships")

Then, on policy violation, ZenStack would return the following:

denied by policy: user entities failed 'create' check
Code: P2004
Meta: { reason: 'ACCESS_POLICY_VIOLATION', message: 'Only admins can create memberships' }
ymc9 commented 1 day ago

I think this will be a very useful feature, but not straightforward to achieve. The reason behind is when evaluating access policies, ZenStack combines all rules in a model together and injects into a Prisma query (for better performance); so when a violation happens, it doesn't really know which rule caused the violation.

Maybe we can consider introducing a "debug" mode just for diagnosis purposes. When that mode is ON, when a violation occurs, it automatically retries the same operation by applying the policy rules one by one to know which one allowed/denied it.

LilaRest commented 1 day ago

ZenStack combines all rules in a model together and injects into a Prisma query (for better performance); so when a violation happens, it doesn't really know which rule caused the violation.

Doesn't Zod provide paths inside of error's issues? That could help linking the Zod output to the specific field that led to the error.

https://zod.dev/ERROR_HANDLING?id=zodissue

ymc9 commented 18 hours ago

ZenStack combines all rules in a model together and injects into a Prisma query (for better performance); so when a violation happens, it doesn't really know which rule caused the violation.

Doesn't Zod provide paths inside of error's issues? That could help linking the Zod output to the specific field that led to the error.

https://zod.dev/ERROR_HANDLING?id=zodissue

Most of ZenStack's access control enforcement is not done via Zod in the Node runtime, but through injecting into database queries (and thus evaluated by the database, through Prisma). For example, when evaluating an "update" rule during an updateMany call, instead of pulling the records out of the database and checking which rows meet the update condition, ZenStack injects the "update" policies into the where clause of updateMany, so the filtering and updating can be efficiently done by the db altogether.

This is even the case for "create". You may have a policy like this:

model Post {
  owner User @relation(...)

  @@allow('create', !owner.banned)
}

To evaluate the "create" condition, we actually need to access its owner relation. ZenStack's general approach is to, inside a transaction, create the entity and then see if we can read it back with the "create" policies as filter; if not, revert. In some cases, e.g., when the create policies don't involve any relation, we internally short-circuit the database read with an in-memory check. But it's considered an internal optimization, and the general approach is to rely on the database-side evaluation.

So given this approach, generally speaking we don't really know which specific policy failed a mutation, as they are evaluated as a whole on the db side ... In some specific cases we can, but I'm not sure how to wrap it into an intuitive feature 😂

This documentation has a bit more background information: https://zenstack.dev/docs/the-complete-guide/part1/under-the-hood