How to build conditional permissions for the GRANDstack

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
- Your graphQL API is secured; and
- 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
- You imported and configured the
graphql-auth-user-directives
package already: this allows you to use the directive@hasScope
- 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:
- The key is a combination of the object in question, in this case
book
, and the condition, in this caseisOwner
. 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. - 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. - The query ends with a
WITH
statement that defines the variableis_allowed
. You must end the query that you return with aWITH
statement that defines the variableis_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 anis_allowed
is found to betrue
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:
- 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.
- 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!