New "How To" for implementing flexible authorization

Hello guys,

I’m the creator of the ZenStack project. I had the chance to meet with some wonderful folks from the Redwood team at the RedwoodJS Conf back in Sept. There I also gave a quick demo to Tom and Rob (@rob I believe :smile:) about how it feels to model complex authorization using the toolkit. As we’ve released several more iterations since then, and the project is in a stable V1 now, I think it makes sense to propose an integration guide into Redwood docs now.

Just a quick recap about ZenStack. It uses an extended Prisma Schema Language to model data and access policies together. It allows you to model RBAC, ABAC, or mixed. For example:

model Post {
  author User @relation(fields: [authorId], references: [id])
  authorId Int
  published Boolean

  @@allow('read', published)        // everyone can read published posts
  @@allow('all', author == auth())  // author has full access
}

At runtime, you create an enhanced PrismaClient, which automatically enforces the policies. It can be used as a drop-in replacement to the regular PrismaClient.

// `db` has the same typing as `prisma`
const db = enhance(prisma, { user: context.currentUser });

Before starting to work on a “How To” PR, I wanted to first check if the team likes the idea and, if so, whether the integration approach that I’m thinking of makes sense.

Here are the current thoughts in my head:

Set up

  • ZenStack provides a @zenstackhq/redwood package for supporting project setup and runtime.
  • Setup should be as easy as running npx @zenstackhq/redwood or yarn rw setup package @zenstackhq/redwood. I’m not sure if the latter is the preferred method.
  • What the setup does is:
    1. Install yarn dependencies
    2. Copy “db/schema.prisma” to “db/schema.model” (ZModel is the extended PSL)
    3. Install a GraphQLYoga plugin like:
      // api/src/functions/graphql.ts
      export const handler = createGraphQLHandler({
          ...
          extraPlugins: [useZenStack(db)]
      });
      
      The useZenStack plugin creates an enhanced PrismaClient for the current user and adds it into the GraphQL context as context.db.

Usage

For developers, only two main differences:

  1. Instead of modeling in db/schema.prisma, do it in db/schema.zmodel. Then run npx zenstack generate and schema.prisma will be regenerated (together with other code for supporting access policy enforcement). All Prisma workflows, like migration generation, db push, etc. stay unchanged.
  2. Instead of using db in GQL services, use context.db instead for access policy enforcement.
    export const posts = () => {
      return context.db.post.findMany()
    }
    

Btw, we should probably wrap zenstack generate into yarn rw zenstack generate. My understanding is that the CLI commands are not pluggable so it’ll require changes to the Redwood code base. Right?

Other notes

Developers can continue using the @requireAuth to guard the GQL queries and mutations, but it’s optional if the ZModel has fully expressed authorization requirements.

The Redwood RBAC can be used together with ZenStack. Developers can still use “roles” to guard frontend routes, and they can also use “role” to define policies in ZModel.

Demo project

You can find a fully implemented multi-tenant Todo app here.


Please let me know if you feel the proposal aligns with the Redwood team’s plan for the documentation. And I’d appreciate any suggestions for how to make the integration more frictionless.

I’ll be happy to make a PR if you think it’s the way to go! Thanks!

5 Likes

Hi @ymc9
Thanks for your detailed proposal! I think this all sounds really good, but let bring it up with the team.

  • Setup should be as easy as running npx @zenstackhq/redwood or yarn rw setup package @zenstackhq/redwood. I’m not sure if the latter is the preferred method.

Both would work, but we’d prefer yarn rw setup package @zenstackhq/redwood

Btw, we should probably wrap zenstack generate into yarn rw zenstack generate . My understanding is that the CLI commands are not pluggable so it’ll require changes to the Redwood code base. Right?

It’s not fully pluggable yet, but it’s something we’ve been talking about and wanted forever! Maybe we can work together to make it happen :slight_smile:

I’ve been keeping my eye on ZenStack for a while and like what you’re doing with it. But as I said, I do want to check with the team before I promise too much :smiley:

We have a team meeting scheduled on Tuesday – I’ll make sure to bring it up for discussion

1 Like

Thank you for your attention and reply @Tobbe ! Please take your time to discuss it with the team. I’m excited about making Redwood a more versatile framework!

Hi @ymc9 and thanks for thinking of having ZenStack support RedwoodJS.

We’ve had devs often ask about the abstract model and field data validations before – and inquire about using ZenStack.

I had a quick look at:

https://github.com/zenstackhq/sample-todo-redwood/blob/main/api/schema.zmodel

And I think it might be helpful if you and I got on a call to chat about the implementation and the RW integration (especially the Yoga plugin) – and how things like releases line up when Prisma/RW/ZS update.

Having a how to would be a nice addition for those who want to use ZenStack; let’s see how we can make this happen.

I’ll try to reach out to you directly to coordinate.

Hi @dthyresson ,

Sure, a call would be great! Talk to you soon!

Hi @ymc9! ZenStack looks super cool.

One thing I would find valuable as someone first hearing about ZenStack is a clearer description of what problem it solves from an auth perspective - from skimming the docs, my understanding is that while ZenStack doesn’t necessarily enable any new behavior in Redwood, it can make doing complex auth much more straightforward (and the automatic CRUD API is also very cool, although maybe less useful for Redwood given the tight GraphQL integration/impending shift to RSC).

So what I would find useful is before/after examples of without/with ZenStack to perform certain tasks, and also a clearer definition of new behavior that ZenStack can enable.

Hi @arimendelow ,

You’ve made a very precise summary! Yes, in the context of RedwoodJS, ZenStack’s participation consolidates authorization to the ORM layer (declaratively) and makes it much more flexible compared to the RBAC today. I feel it’s most needed for people who are building SaaS-like apps.

One of the design goals of ZenStack is to make it straightforward to adopt in existing Prisma projects - for which RedwoodJS projects are perfect examples :smile:. As you said, other high-level features like automatic CRUD APIs and frontend hooks are more for SPA apps and less relevant here.

Yes, we should definitely give good context about what problems it solves in the guide, and it’s a great idea to illustrate it in a before/after fashion.

1 Like

Any update on this one? @dthyresson @Tobbe? :slight_smile:

You’re free to use ZenStack in your applications but at the moment the team is not actively working on that integration. ZenStack did begin their own but did not seem to complete it.

RedwoodJS provides an authentication mechanism as well as very customizable GraphQL validator directives to perform any custom rules your api requires.

Also, one feature of ZenStack, being able to compose Prisma schema across multiple files appears to be coming natively to Prisma soon.

Of strongly suggest looking at what can be done in services and directives first as it’s natively supported and the team will maintain it.

1 Like

Thanks for the reply! :smiley:
The feature I like the most in Zenstack is the ability to inherit models

Me too, I found that polymorphism isn’t supported in prisma and hasn’t been on the roadmap for 4 years. With ZenStack v2 coming out and supporting it I’m making the switch now. I use a custom directive to enforce permission rules but having to write it in the sdls is more time consuming than doing it the ZenStack way in the prisma schema itself. I can also write much more complex rules.

For my production environment if I don’t want to do unions in my sdl I depend on ZenStack’s polymorphism implementation. I’m glad Prisma is finally delivering the multi schema support, though. While I don’t see myself using the auto CRUD stuff since Redwood handles that itself, it might be useful when RSC drops depending on how CRUD is implemented. I’m amazed at how the ZenStack team responds to developer feedback and what they’re prioritizing.

I hope that perhaps a Redwood integration might still be possible. Thank you for the hard work you do @ymc9

1 Like

FYI - full support in Redwood is coming in the next release. We had to do some rework in generators etc to have better compatibility: feat(prisma): Support multi file Prisma schemas by dthyresson · Pull Request #10869 · redwoodjs/redwood · GitHub

Could you share? I’d be curious to see if anything can be done to make directives easier to write.

Also, you can use Prisma client extensions to do pretty much what ZenStack was doing. They sorta of had a DSL-like parser on schema to then auto create client extension-like logic on models.

That’s the best way to have db-level permissions tag moment.

Maybe I should’ve rephrased that. I don’t doubt you can write very complex authorization rules with directives or using client extensions. From a DX I’ve just really enjoyed opening up my schema and being able to see immediately what’s going on. For example, I had a permission directive that checked if the authenticated user had a specific permission (auth check included in this new directive and I removed requireAuth essentially).

User → Role → Permissions[{ name: string }]

type Query {
    schools: [School!]!
      @requirePermission(permissions: "read:school:any")
    school(id: Int!): School
      @requirePermission(permissions: "read:school")
  }

I was adding this directive to sdl files for every model and further checking context in the resolver. You can imagine that’s quite time consuming with over 20 models and doesn’t give me an overview immediately. Instead, I have to click every SDL to see what permissions I’ve given for each specific service resolver. Sure that’s more fine-grained but something like this is making my DX a little simpler and still achieves the same authorization:

model School {
...

@@allow('read', auth().roles?[permissions?[name == 'read:schools']])
@@allow('delete', auth().roles?[permissions?[name == 'delete:schools']])
}

I could imagine some kind of meta file that’s processed where you could define your queries and mutations with appropriate authorization checks could solve this but that’s probably a custom client extension I’d have to create. Go to acl.ts and define all requirePermission() for each query and mutation that exists in my project. Would be kinda cool and I’d switch back from ZenStack’s implementation, but alas…

I’m still experimenting with what’s possible and performance is also going to be a major test once everything is migrated to the models. I definitely need to go deeper into client extensions as I’ve read that doc multiple times but in my beginner brain nothing is computing. The examples don’t lend themselves to what I’m trying to do and I’ve watched prisma’s video on extensions and I kind of lost it at the K and T types. Not giving up, but it’s greater learning curve than I anticipated.

On a side note, this was also interesting to read and what is possible with ZenStack: Checking Permissions Without Hitting the Database (Preview) | ZenStack

I’m definitely looking forward to seeing what the prisma multiSchema looks like, the experience so far with ZenStack has been very fruitful. Using abstract models and consolidating widely used enums into a .zmodel file and then inside a folder ensures I stay DRY. When I look at a model now it’s inside a nice folder structure and it imports anything that relates to it. Then, ZenStack’s CLI just magically creates a one file prisma.schema and I’m good to go.

1 Like

The DX is a little better if you create an enum for those permissions instead of using strings.

import { createValidatorDirective, ValidatorDirectiveFunc } from '@redwoodjs/graphql-server'

import { requirePermission as applicationRequirePermission } from 'src/lib/auth'

export const schema = gql`
  enum Permission {
    READ_SCHOOL_ANY
    READ_SCHOOL_ID
  }
  """
  Use to check whether or not a user is authenticated and assigned one of the given permissions
  """
  directive @requirePermission(permissions: [Permission]) on FIELD_DEFINITION
`

const validate: ValidatorDirectiveFunc = ({ directiveArgs }) => {
  const { permissions } = directiveArgs
  applicationRequirePermission({ permissions })
}

const requirePermission = createValidatorDirective(schema, validate)

export default requirePermission

usable like this:

type Query {
    schools: [School!]!
      @requirePermission(permissions: READ_SCHOOL_ANY)
    school(id: Int!): School
      @requirePermission(permissions: READ_SCHOOL_ID)
  }

and then you can also create a dictionary with the gql type to use outside of the sdls.

import { type Permission } from 'types/graphql'

export const Permissions: Record<Permission, Permission> = {
  READ_SCHOOL_ANY: 'READ_SCHOOL_ANY',
  READ_SCHOOL_ID: 'READ_SCHOOL_ID',
} as const
1 Like

Hey guys,

I’m the one who proposed it. My apologies for the late follow-up. It seems our discussion got interrupted on Slack @dthyresson :smile:.

ZenStack actually released a redwood integration package here:

And a guide here: Using With RedwoodJS | ZenStack

However, it was made before the release of the RW V7. A community member mentioned a break caused by the change of where GlobalContext is declared in V7. After adapting to that, he’s got it working end-to-end. I’m not sure if that community member is you @jamesj :laughing:

We’ll make an update for RW v7 in the next minor release. Will update back here soon.

1 Like

Yes that was me :joy: It’s been a blast using ZenStack, the abstract models and schema splitting have been wonderful and working well with well over 100 models. The only other issue I noticed during deployment with Docker-compose (on Coolify) was that I had to add a copy in my Dockerfile during api_build for the .zenstack folder in production. Otherwise I’d get an error saying enhance() was missing. I do run the @zenstackhq generate command during deployment but I also know you can specify the output as mentioned here Deploying to Production | ZenStack

However we don’t directly use the enhance() import with Redwood so I’m not sure how that would work.

1 Like

Good to know the progress! I’ll follow up with you on the docker issue.

A quick update: the latest ZenStack v2.4.x supports Redwood V7 now!

Thanks @jamesj for piloting the upgrade.

2 Likes