How to build conditional permissions for the GRANDstack

Nathan Meibergen
7 min readDec 2, 2020

If you got to this article, you are probably looking for a way to implement non-static permissions into your GRANDstack application. Permission - or scopes - are usually in the form of object:action, however in some situations that’s just not enough: what you might need is object:action if a user has some relationship to the object. This is what I call a conditional permission. It might seem like a trivial requirement. Unfortunately it is not.

In this article I’ll show you how to implement conditional permissions such that

  1. Your graphQL API is secured; and
  2. Your REACT front-end is well integrated, that is, you don’t want to show a button to a user that is not allowed to use it!

My package graphql-auth-user-directives has a new feature that allows for an easy implementation of such conditional permissions. If you haven’t used this package before I strongly advice you to first go through the basics of this package before diving into this more advanced topic. The basics are described in detail in my previous article.

The starting point for this article assumes you already have an authorization flow based on JSON web tokens (JWTs) integrated for your GRANDstack application.

Feel free to immediately dive into the Github repo.

Conditional permission — an example

Let’s start with an example of what a conditional permission is. Consider the following authorization scheme:

The idea behind this scheme would be such that an admin can create a new book and if he/she wants to edit a book he/she can. However, what if we only want to allow an admin to edit a book they created themselves? What we need is a conditional permission, let’s write this as: book:edit:isOwner. This notation implies that after the second colon we write the condition. You can read this as: an admin can edit a book if it is owner.

Next I’ll show you all the steps you need to take to implement the conditional permissions.

Configuration

Let’s configure the conditional permissions based on the example we just presented: I will show you how to implement a mutation to edit books, where we allow admins access if they are owner of the book.

Create a scheme with conditional permissions

First, update the authorization scheme to include some conditional permission as needed for your case. Below is an example of how you could implement multiple such conditions:

You can use capital letters if you want, or use a space of the colon, that’s all fine.

Adapt these scopes at the place where you store them, that is, if you use Auth0 to store scopes, change them there, if you use any other way of storing the scopes use that. The important thing here is that in the end the decrypted JWT token should contain scopes. I’d suggest using the approach explained in my previous article, but that’s not required.

Define your graphQL schema

We just show an example of the editBooks mutation:

To do this, we assume that

  1. You imported and configured the graphql-auth-user-directives package already: this allows you to use the directive @hasScope
  2. You use the neo4j-graphql-js package, for this, among others, allows for the @cypher directive.

As you can see we don’t have to specify the conditions: the package will understand that if the provided permission is book:edit it should check all conditions assigned to the user, for example, for an admin it will check isOwner and created. If any of these conditions are valid the admin will be able to access this mutation. Clearly, if you’d like to specify an exact conditional permission in the GraphQL schema you are free to do so, for example, there is nothing wrong in specifying a mutation with @hasScope(scopes:["book:edit:isOwner"]) (I’m not sure when that would make sense, but the package will understand what you mean).

Define the query for a condition

The package graphql-auth-user-directives exports a function called conditionalQueryMap. This is a javascript Map object which as a key takes the condition and as value a function. Take the following example:

There is a logic to this example:

  1. The key is a combination of the object in question, in this case book, and the condition, in this case isOwner. The notation will always be of the form: object:condition, where these are related to how you have defined these in your scheme — all white spaces are removed.
  2. The value is a function that takes two arguments: the user and an objectId. The user that you’ll have at your disposal is the one that has been decoded by the JWT decoder (for more info please see my previous article). The objectId is the id of the object that is under consideration. Wait, what? I know, it needs some extra explanation, see the next subsection.
  3. The query ends with a WITH statement that defines the variable is_allowed. You must end the query that you return with a WITH statement that defines the variable is_allowed. The reason being that if you have multiple conditions that should be verified we want to do this in a single query to Neo4j. All conditions are concatenated in such fashion that if an is_allowed is found to be true for one single condition the resulting user is provided access.

On the objectId

Let’s go back to the example of editing books if you are an owner, that is, the permission/scope books:edit:isOwner. What do we mean by objectId and what is the object under consideration? Well, when you define your graphQL schema you will define queries and mutations. For this example the editing of books will be a mutation as shown in the above defined graphQL schema. There are two things you might note in this mutation:

  1. This mutation is all about performing an action on some object. The action is editing, and the object is the book. This is also the logic that we use for the scope.
  2. To perform this mutation we are providing an id, and it makes sense that this is the book id. This is what we mean with the objectId that is being passed into the value of the conditionalQueryMap.

Usually the id is easily identifiable by the graphql-auth-user-directives because it is simply called the argument id or uid. However, maybe in your case you use another identifier. If you’d like to change the identifier you can set the following environment variable:

export OBJECT_IDENTIFIER="<your_ids>" # defaults to "id, uid"

Note that, just as in the default case you can provide multiple identifiers, separated by a ,. The order is important in that it will first look for the first identifier followed by the second, etc.

Integrating the permissions into your React front-end

If you made the above configurations for your case you will have an graphQL API secured with conditional permissions, however at some point you may also want to implement these permissions into your front-end. For example: suppose again the book:edit:isOwner permission. In your front-end you might want to create a button to show the user that he/she can edit the book, but clearly, only if this user is the owner of the book. Of course, with the above configuration you already made sure that only owners can call the corresponding mutation, however the front-end should also be aware of this permission. How to proceed?

The graphql-auth-user-directives packages exposes a function to check conditional permissions. This is what enables you to verify conditional permission by creating a GrahQLquery to check permissions in the front-end. Let’s implement this.

The idea for this implementation is that we will create an access-control component, that surrounds any piece of React code that you’d like to secure. This access-control component will check whether access is granted or not. It will do so by calling a React hook. Let’s start by creating the React hook.

Create a React hook to check permissions

The react hook that we create can be used in the access-control, but also in places where you don’t need an access-control but merely want to know whether some permission is satisfied. The hook is called useCheckRules and is defined as follows:

Let’s walk through the code. We start by defining a query to verify conditional permission with Apollo. In the next subsection we will show the graphQL API for this query.

The useCheckRules hook is called with the argument action and objectId. The action refers to the non-conditional permission that you’d like to verify, for example,book:edit and the objectId refers to the id of the object in case (see the previous section), thus the id of the book in the case.

To run this hook you’ll need user information, in particular the scopes of this user. This can be the result of decoding the user with the graphql-auth-user-directives package: at initiation of your react code the user should be retrieved from the backend including its scopes. If you use Auth0, this is just a small adaption of their useAuth0 hook.

The rest of the useCheckRules hook determines whether the user has exactly the provided permission, if so, access in granted. If it is found that the user has a conditional permission related to the action, then this conditional permission is verified in the database by means of the lazy query.

Set up the GraphQL conditional permission check API

As seen in the previous subsection we need the graphQL endpoint called checkConditionPermission. Create this in your graphQL schema. See the below example.

Next, define the resolver as follows:

As you can see we use the function satisfiesConditionalScopes from the graphql-auth-user-directives package that performs the check on whether the user is allowed to the object that it wishes to see.

Create the access control component

Finally, to put everything together, create the component AccessControl. This is a simple component that uses useCheckRules to render children or some statement if no access is allowed.

To use this acces control component all you need to do is wrap the component that you want to ‘secure’ with the AccessControl component and provide information on what to do if no access is granted.

That’s it!

If you have been able to go through all coding, you’ll have created a fully integrated authorization system for your web application. This authorization system is very flexible with regards to setting up permissions for your users, see my previous article, and in addition you can now handle conditional permissions in you Neo4j database.

Thank you for reading. If you have any feedback, tips&tricks or need more help, let me know!

--

--

Nathan Meibergen

I am a mathematician, enthusiastic about creating analytics driven tools using state of the art analytics models and development tools.