zenstackhq / zenstack

Fullstack TypeScript toolkit 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
1.83k stars 78 forks source link

[Feature Request] Count relation fields for access control #1504

Open baenie opened 2 weeks ago

baenie commented 2 weeks ago

Is your feature request related to a problem? Please describe. It would be nice, if its possible to count a relation field in a model and create access policies with it. This way the access control could be extended even more, e.g. deny create permissions on the Space model, if the auth() already has 3 spaces.

Describe the solution you'd like

@@allow("create", auth().spaces < 10)

// with filtering
@@deny("create", auth().spaces[type == "pro"] >= 2)

// limit a space to have only have 10 members max
@@deny("update", future().members > 10)
ymc9 commented 2 weeks ago

Hi @baenie, Thanks for filing this. I agree that it's very useful to be able to use relation's count to define policies. There will be some challenges to having a generic implementation (supporting both read and write policies) due to Prisma's limitations.

Meanwhile, I'm also wondering if we should provide a more generic extensibility for policies. My current thoughts go in the direction of allowing developers to define custom functions to participate in the policy evaluation. For the case of checking "create" rules, the function is passed with the Prisma transaction used to do the create, the auth() value, and the newly created (but not committed yet) entity value. This will allow one to make extra database queries and checks and finally determine if the operation should be granted or rejected. I'm showing some pseudo-code below:

model Foo {
  @@deny('create', !spaceCountCheck())
  ...
}

// function declaration in ZModel
function spaceCountCheck() {}
const db = enhance(prisma, { user: ... },
  {
    customFunctions: {
      // function implementation in TS
      async spaceCountCheck(tx: PrismaClient, user: User, entity: unknown) {
        // `tx` is the transaction used to create the entity
        // `user` is the `auth()` value
        // `entity` is the created (but uncommitted) new database entity
        // the function return a boolean value to indicate if the operation should be granted or rejected
        return user.spaces.filter(space => space.type === 'pro').length <= 1
    }
  }

I feel this should be able to cover your needs, although more complicated than your original proposal. I'd love to hear your thoughts about this direction.

baenie commented 1 week ago

Thank you @ymc9 for your response!

I really like the idea of having a generic extensibility with custom functions for the access control! This could help making it as secure as possible, without having issues with Prisma limitations after all (and sadly there are a few of them 😅).