Search code examples
reactjsmongoosegraphqlrbaccasl

RBAC on GraphQL backend using CASL and graphql-shield and sharing rules with my React front-end


I am running Mongoose and exposing an API using GraphQL (Apollo).

I want to implement a RBAC and after some research I came with a solution using CASL and graphql-shield. Ideally, I would then want to share the rules with my React front-end.


First step, planning on a piece of paper.

I would first define my actions: Create, Read, Update, Delete.

Then I would define my subjects: Car, Motorcycle.

After that is done I would proceed to define my roles: CarSpecialist, MotoSpecialist, Admin. I would then define some conditions: "subject is my own", etc..

Finally, I would assign to each role, a set of abilities (combination of action, subject, conditions).


Now with all this done, I start actually coding my solution.

I start by writing the abilities in CASL: actions and subjects are pretty straightforward to define.

Conditions are a bit trickier and I have at least two options:

  • I use "vague" notions that in turn have to be interpreted by whatever needs to enforce them (back or front end).

  • I use the CASL mongoose integration plugin, at the cost of losing the ability to share with my frontend.


Any input on which to choose?

Now once CASL abilities are defined, is it up to graphql-shield to enforce them?

How do I do the mapping between (CASL) actions, subjects and conditions to graphql terms: Schema, Query, Mutations ...?


Solution

  • I’ll try to answer on this question as much as I can:

    1. You don’t loose capability to share permissions with UI if use default conditions. Conditions are interpreted in js when you run ability.can. So, if mongo query language is fine for you, then no need to change it!
    2. Graphql shield is a special kind of graphql middleware. If you use casl and graphql middlewares, you don’t need graphql shield! use either casl + custom graphql middleware or graphql-shield
    3. Every graphql type has underlying source type. Source type is basically your domain model or just db model that encapsulates business logic. This is your mapping :) just check permissions on source type and that’s it. But if you share permissions with UI, then you need to transform backend permissions (before sending to UI) which are written for source types, to those that can be applied to graphql type! Alternatively, you can expose some private props (e.g., ownerId of Car) as part of graphql type. But if the only purpose of this is to satisfy permissions sharing, then I’d go with transformation option:
    function defineAbility(user, props) {
       const { can, rules} = new AbilityBuilder(Ability)
    
       can('read', 'Post', { [props.authorId]: user.id })
       // ...
    
       return rules;
    }
    
    const currentUser = { id: 1 }
    const backendRules = defineAbility(currentUser, {
      authorId: 'authorId'
    });
    
    const uiRules = defineAbility(currentUser, {
      authorId: 'author.id'
    });
    

    Alternatively, you can check permissions on backend and share results with frontend by exposing subtype on every graphql type:

    
    {
       cars {
         items {
           permissions {
             canUpdate
             canRead
           }
         }
       }
    }
    

    The consequences of this is that your server will spend more time generating response, especially when you retrieve items for pagination. So, check response times before proceeding with that. The good point of this is that you don’t need casl on UI, and permissions checking logic is completely hidden on backend