Complex Permissions: Hybrid ABAC -> RBAC

We have an in-app permissions setup, which is familiar to all of us, but does not seem to be as easy to config as one would hope.

Basically, it is permissions like you are used to with google drive, airtable, slack, your PM/task tools, etc … and that is that you give someone access to a certain type of asset, with a certain permissions level for that asset.
Additional, this permission is usually inherited to child assets in the hierarchy with the same permissions, unless a child has their permissions over-ridden as private, etc.

The access to these assets is based off the asset “type” being shared with other users, within a hierarchy, and for certain permissions (ie crud, etc) for each asset that is shared with the other user.

To us, this seems to be hybrid ABAC → RBAC … as in, the user gets permissions to a certain assetID and type, and for that assetID and type, they are granted certain roles, for that asset type.


For us, we have a number of assets within a hierarchy, such as:

  • organisation {one:many} child-organisation
  • project {one:many} child-project
  • dashboard {one:many} tile {many:many} data-connector, etc …

And for each asset type, we have certain roles. such as:

  • owner, manager, collaborator, guest-edit, guest-view, public, etc

This means that the OWNER of dashboardID9akje86874, within projectID987365, which is within orgID362, wishes to share that dashboard with userIDkjshu8r6783 and give them the permissions of a guest-view for ONLY dashboardID9akje86874.

userIDkjshu8r6783 can only view the dashboard, they are unable to see detail of the org or the project, unless it is displayed on the dashboard.
(like when someone shares a file or folder with you from google drive, you can see what is within that asset that is shared with you, but not the parent structure/assets).

That same OWNER has permissions to add someone else to projectID987365 with collaborator permissions on the project. They would then be able to see the project details and all the dashboards in the project.


We are currently investigating the following libraries, which seem to solidly support hybrid ABAC/RBAC permissions scenario:

Has anyone looked at these, and any thoughts on which may/may not work best with RedwoodJS ?

Thanks in advance for any thoughts, advice or assistance on how to approach.

Max


ps: have looked at other threads, but have not seen anything beyond a mention of CASL (which we looked at, but didn’t seem to extend far enough).
pps: we had been playing with blitz also, and there is no solution (less evolved than redwood) there without external libraries either. The approach in schema base only takes so far in both, and is easy to have this in the prisma2 structure (and prisma is great to help with this once you work it out), but the per asset, per type, then roles on tops of that being managed in a scalable way, then retrievable in the UI for a userID, or an assetID, and for sharing on individual asset in a hierarchy needs a bit more heavy lifting (and my be more than what we can spin up ourselves with any surety we haven’t stuffed it up).
Would love to work this out, as will help us a HUGE deal to move forward, as permissions is where we are blocked at the moment :frowning:


Refs:

2 Likes

@MaxLynam

As you know, with most access control some common questions to solve are:

  • how to define the access permissions
  • how to store the access permissions
  • how to trust those permissions
  • how to fetch them
  • and how to enforce them

Depending on the authentication provider and other choices you can make when designing an app, those questions can be answered a number of different ways.

At the moment RedwoodJS supports several Auth providers and they in turn deliver a JWT (that can be verified) that can contain some bits of information. The access info is just that – info.

It determines that they are authenticated and for how long and some identifier that represents them.

In some cases, like with Netlify and Auth0 they can store some additional info on the user_metadata that indicates what roles or permissions they have. What those are can be for the app to decide.

The auth provider and token won’t do any enforcing – it will just give you info that can be used to enforce those rules.

The providers at the moment that RedwoodJS supports that has the most fine grained control that I have seen are Auth0 for the info on the JWT and Supabase for its data-level policies.

Auth0 can store and set both roles and permissions on the auth token. But, again you’ll have to enforce those rules either at the data store level or at the service level. That’s where your ABAC rules and “enforcers” can come in.

Supabase can provide policy-level control at the database level by enforcing read/write permissions based on a set of rules/queries.

image

This way you can determine who the user is via the JWT, match their roles, and enforce a policy when selecting data. Ie - don’t let this user access content they are not allowed to. Here you may be able to define your ABAC rules in SQL and enforce them. The nice thing about this is that it’s at the database level. However, in RedwoodJS case here it typically connects to the db via an admin account so one may have to use the Supabase SDK client to query or at least check permissions.

From my very very very quick scan at casbin, it looks similar to Supabase’s policies – you defines some read/write access to a model and there a function that returns a true/false based on some logic to determine access.

R/ABAC is non-trivial and my best suggestion is to create a small proof of concept app with 5 or so of your main uses cases and try out a few possible tools or techniques. And write test cases to make sure that you can reproduce the right access permissions each time as you try out each one.

You’ll get a quick sense of if it works for you, how difficult it is to maintain/manage, and if you like developing with it.

Let us know how you get on. Curious to know what you find out.

3 Likes

thanks heaps @dthyresson … it is definitely a beast and we are doing a few trials to see what seems to be doing what we need.

Will also follow on from your instructions and check Supabase … hadn’t seen that before and looks pretty cool :slight_smile:

Thanks heaps for the extra guidance.

1 Like

Hi @MaxLynam ,

I found this discussion when writing my blog post about using ZenStack in RedwoodJS projects. It’s been over two years, so not sure if you’re still interested in the topic. Just in case.

The ZenStack toolkit that we’re working on is an extension of Prisma ORM, and one of the most important extensions is the addition of modeling access control. So this provides an alternative to where authorization is implemented. The schema looks like this:

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  comments  Comment[]
  user      User     @relation(fields: [userId], references: [id])
  userId    Int
  createdAt DateTime @default(now())
  published Boolean @default(false)

  // 🔐 Admin user can do everything to his own posts
  @@allow('all', auth().roles == 'admin' && auth() == user)

  // 🔐 Posts are visible to everyone if published
  @@allow('read', published)
}

model Comment {
  id        Int      @id @default(autoincrement())
  name      String
  body      String
  post      Post     @relation(fields: [postId], references: [id])
  postId    Int
  createdAt DateTime @default(now())

  // 🔐 Moderator user can do everything to comments
  @@allow('all', auth().roles == 'moderator')

  // 🔐 Everyone is allowed to view and create comments for published posts
  @@allow('create,read', post.published)
}

Here’s the post for people who may be interested: Implementing Flexible Authorization in RedwoodJS Projects | ZenStack.

2 Likes

Thank you, that’s a great library. You need to add the Redwood logo on your home page “Integrated With The Tools You Love” section :slight_smile:

Thanks! Yes, we should. Just thinking of building a sample that is a bit more complex than blogging first :smiley:

I found this discussion when writing my blog post about using ZenStack in RedwoodJS projects. It’s been over two years, so not sure if you’re still interested in the topic. Just in case.

Wow, I’m excited to see this @ymc9! I’ve only just skimmed your docs a little, but it looks like a very elegant solution! I’m particularly drawn to the fact that I don’t need to mess around with a SaaS or Postgres settings - I feel very out of my depth in that area.

I think for the Redwood integration for the access control bit, I’m guessing we’d need a way to pass the functions it uses to determine the AuthZ or AuthN? I’m sure you have a way of doing it, but just not clear to me from the initial read.

Just as suggestions on what to explore:

  • typically, the code for checking AuthN sits in requireAuth in src/lib/auth.ts
  • requireAuth can take an optional role, which usually is checked against the JWT (depending on the auth provider)

If this isn’t ideal (particularly in terms of DX) - would love to hear alternative approaches!

1 Like

Hi @danny , thanks for checking out ZenStack, and I’m super glad you like the idea!

I do have a very simple demo project experimenting with the integration with a Redwood project (based on the tutorial blog app): GitHub - zenstackhq/sample-redwood-blog: The end state after completing the RedwoodJS introduction tutorial.

The approach is actually quite simple, with two steps:

  1. create a helper that returns an “access-control-enabled” Prisma Client for the current user in context

    // api/src/lib/db.js
    
    export function authDb() {
      return withPolicy(db, { user: context.currentUser })
    }
    

    The withPolicy API returns a proxied Prisma Client that enforces access control rules. The user field passed in the context object determines the return value of auth() call in the schema. E.g.: @@allow('all', auth().roles == 'moderator')

  2. in the service code, use the authDb() helper to get a db instance instead of the raw Prisma Client

    /api/src/services/comments/comments.js
    
    export const deleteComment = ({ id }) => {
      return authDb().comment.delete({
        where: { id },
      })
    }
    

I think whether or not we keep using the requireAuth helper depends on how much rules you put into the schema. If all isAuthenticated and role checks are expressed in the schema, we may not need it anymore.

There’re still several open issues:

  1. Whether there’s a way to fetch db instance from a piece of centralized code instead of making authDb() calls everywhere
  2. How to intercept “authorization policy rejection” errors globally and turn them into proper ForbiddenError
  3. How to check permission from the frontend (a missing feature from ZenStack)

I’m quite new to Redwood, so I probably also overlooked other important things.

I love Redwood’s idea of giving developers a battery included framework and feel ZenStack to to be a good fit into the picture.

Concerning the authDb call, I think it should be possible to do the following:

export const authDb = withPolicy(db, {user: context.currentUser })
import {authDb as db} from "src/lib/db"

At least I am able to get the context within db.js, when I define an extended prisma client, so I think this should be possible, too.

I am really curious about zen stack, but do not dare to dig into it too much right now. Could solve many of my issues, but might create new ones. But going to keep an eye on it and see how it gains traction! The redwood integration definitely lowers the threshold! :slight_smile:

Hi @dennemark , thanks for the suggestion. I think the problem is that the authDb instance needs to be local per request because it’s bound to the current user context, so it cannot be a global export.

I completely understand your concern, and “solving problems without introducing new ones” is a goal for us, and I really wish to achieve that :smile:.

Oh you might be right… now I am also curious how to avoid the extra calls… :smiley: I guess it is also not very helpful for transactions and batching of prisma…

But one thing I am doing, is setting the context in graphql.ts on the createGraphQLHandler. And db is able to receive context in that case. Is the createGraphQLHandler not called on every request?

Just to clarify a bit, the withPolicy call actually creates a wrapper around the given Prisma Client instance instead of creating new ones inside. So, there’s still only one single Prisma Client globally, and batching/transaction behavior should stay the same.

I haven’t looked much into createGraphQLHanlder, but it appears to be creating a singleton. Will make a further check about it. Thanks!

1 Like

ZenStack I love it! I’m starting to use it today!!

I’m starting a fork of the Docs to start turning that [above] post into a Docs Quickstart for RedwoodJS

Thanks so much !!

1 Like

Thank you @ajoslin103. I’m glad you find it helpful!

I just wanted to let you know there’s a more formal documentation on on the website for Redwood + ZenStack integration here: Using With RedwoodJS | ZenStack.

It uses the same approach but the helper package makes the process more automated.

already found it! Wish it was linked from the quickstart :slight_smile:

1 Like

I got things working - thanks so much for a wonderful product !!

I saw reference to zenstack_guard && zenstack_transaction

are those a best-practice convention ?

zenstack_guard and zenstack_transaction existed in very old releases. Do you still see them somewhere today?