User authorization is a vital part of any business application. Every developer faces the authorization challenge. There are many ways to implement authorization in Django. Django has a built-in permission-based authorization system, and there are some third-party apps like django-guardian and django-rules.

In our previous article, we implemented Token-based authentication with
django-graphql-jwt
. It has also some useful decorators to implement authorization.

But in this article, I will share a custom way to implement role-based authorization in GraphQL. Maybe in the next article, I will share some other ways of authorizations with Django’s built-in permission module.

Let’s start writing some code.

Basic Setup

At first, go to account/models.py file and add the following code.

from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):

    class Meta:
        db_table = "user"

    username = models.CharField(
        max_length=64,
        unique=True
    )
    role = models.ForeignKey(
        'account.Role', 
        on_delete=models.CASCADE, 
        blank=True, null=True
    )

    def __str__(self):
        return self.username


class Right(models.Model):

    class Meta:
        db_table = "right"
        ordering = ["name"]

    name = models.CharField(max_length=64)
    codename = models.CharField(max_length=64)
    description = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.name


class Role(models.Model):

    class Meta:
        db_table = "role"

    name = models.CharField(max_length=64)
    description = models.TextField(blank=True, null=True)
    rights = models.ManyToManyField(Right)

    def __str__(self):
        return self.name

Here we are extending AbstractUser from Django’s auth module. And added the other two models for Role and Right. Nothing fancy here. Everything is straightforward.

Now go to settings.py and add the following code here.

AUTH_USER_MODEL = 'account.User'

Now run the following commands to generate migrations.

python manage.py makemigrations
python manage.py migrate

Ok. Cool! let’s write some schema for role and rights and make some other changes in mutation.

Writing Schema

Now go to account/schema/users.py file and add the following schema.

import graphene
import graphql_jwt
from graphene_django.types import DjangoObjectType, ObjectType

from account.models import User, Role, Right


class UserFields:
    id = graphene.ID()
    username = graphene.String(required=True)
    password = graphene.String(required=True)
    email = graphene.String(required=True)


class RightFields:
    id = graphene.ID()
    name = graphene.String()
    codename = graphene.String()
    description = graphene.String()


class RoleFiels:
    id = graphene.ID()
    name = graphene.String()
    description = graphene.String()
    

class RightType(DjangoObjectType, RightFields):
    class Meta:
        model = Right


class RoleType(DjangoObjectType, RoleFiels):
    class Meta:
        model = Role

    rights = graphene.List(RightType)


class UserType(DjangoObjectType, UserFields):
    class Meta:
        model = User


class RightInputType(graphene.InputObjectType, RightFields):
    pass


class RoleInputType(graphene.InputObjectType, RoleFiels):
    rights = graphene.List(RightInputType)


class UserInputType(graphene.InputObjectType, UserFields):
    role = graphene.Field(RoleInputType)

We don’t need to make any change to our Query. But CreateUser mutation needs some changes. So, update it with the following code.

class CreateUser(graphene.Mutation):
    class Arguments:
        input = UserInputType(required=True)

    ok = graphene.Boolean()
    user = graphene.Field(UserType)
    
    @staticmethod
    def mutate(root, info, input):
        rights = input.role.rights
        right_ids = []
        for right in rights:
            obj, created = Right.objects.get_or_create(name=right.name, codename=right.codename)
            right_ids.append(obj.id)
        role, created = Role.objects.get_or_create(name=input.role.name, description=input.role.description)
        role.rights.set(right_ids)

        user = User()
        for key, val in input.items():
            if key is "role":
                val = role
            setattr(user, key, val)
        user.set_password(input.password)
        user.save()
        return CreateUser(ok=True, user=user)

Role and Permission Decorator

We will write a custom decorator to check the current user role and permissions. So, add a utility function to blog/utils.py file. Later, we will add a common app and move utility functions here. At the top, import GraphQLError like this

from graphql import GraphQLError 

And add the following decorator function.

def can(*permissions):
    def wrapped_decorator(func):
        def inner(cls, info, *args, **kwargs):
            
            if not info.context:
                raise GraphQLError("Permission Denied.")

            user = info.context.user
            if not user.is_authenticated or not user.role:
                raise GraphQLError("Permission Denied.")

            # An admin (Django superusers) can do everything.
            if user.is_superuser:
                return func(cls, info, **kwargs)

            # A user CAN perform an action, if he has ANY of the requested permissions.
            user_permissions = list(
                user.role.rights.all().values_list("codename", flat=True)
            )

            if any(permission in user_permissions for permission in permissions):
                return func(cls, info, **kwargs)
            raise GraphQLError("Permission Denied.")

        return inner

    return wrapped_decorator

Use Permission Decorator

It’s time to use the above decorator function. Go to `account/schema/users.py` and import the decorator function as following.

from blog.utils import can 

And use the function as a decorator like this

class Query(ObjectType):
    me = graphene.Field(UserType)
    users = graphene.List(UserType)

    @can("manage_own_profile")
    def resolve_me(self, info, **kwargs):
        user = info.context.user
        if user.is_anonymous:
            raise Exception('Please login!')
        return user

    def resolve_users(self, info, **kwargs):
        return User.objects.all()   

Test role and permission

Open graphiql in your browser and add the following mutation.

mutation createUser {
  createUser(input: {
   username: "ijhar_admin",
   email: "ijharislam@gmail.com",
   password: "morning_blog"
   role: {
    name: "admin",
    description: "Person with Admin role will have all the access to the application",
    rights:[
      {
        name: "Manage Own Profile",
        codename: "manage_own_profile"
      },
      {
        name: "Manage Blog",
        codename: "manage_blog"
      },
      {
        name: "Manage Authors",
        codename: "manage_authors"
      }
    ]
  } 
  }) {
    ok
    user {
      id
      username
      email
      role {
        name
      }
    }
  }
}

It will return the following result.

To get the JWT token, add the following mutation.

mutation  {
  tokenAuth(username:"ijhar_admin", password: "morning_blog") {
    token
  }
}

Here we got this result.

{
  "data": {
    "tokenAuth": {
      "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImlqaGFyX2FkbWluIiwiZXhwIjoxNTc5MzI3OTY4LCJvcmlnSWF0IjoxNTc5MzI3NjY4fQ.fvRq8GGfU_DZkj5QywrbASAILYIT52VBt-WYFin4xuU"
    }
  }
}

Now we have to use this token to access any particular information. For example, we added @can("manage_own_profile") decorator to me query. If we want to access the current user’s info from it, it will give following error.

This error message is coming from our permission decorator.

To access this information, we need to pass Authorization header with a valid JWT token for a user who has manage_own_profile permission.

Graphiql does not have support to add a header with the query. So, to test the permission, we will use Postman. Recently postman added GraphQL to their console which is in beta version. So, open postman and add a header like this.

It worked! Awesome. Here we have to add the token with JWT prefix, not with Bearer.

Let’ s create another user, who will not have manage_own_profile permission. So, add the following code.

mutation createUser {
  createUser(input: {
   username: "ijhar_staff",
   email: "ijharislam@gmail.com",
   password: "morning_blog"
   role: {
    name: "staff",
    description: "Person with Admin role will have all the access to the application",
    rights:[
      {
        name: "Manage Blog",
        codename: "manage_blog"
      },
      {
        name: "Manage Authors",
        codename: "manage_authors"
      }
    ]
  } 
  }) {
    ok
    user {
      id
      username
      email
      role {
        name
      }
    }
  }
}

You will get the following result.

Let get the JWT token for this user.

I got this token here.

{
  "data": {
    "tokenAuth": {
      "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImlqaGFyX3N0YWZmIiwiZXhwIjoxNTc5MzI5MzkzLCJvcmlnSWF0IjoxNTc5MzI5MDkzfQ.L9mb4cKX_AXw9upFVhr650xhq2VxKeHgfltXYSa_S70"
    }
  }
}

I will use this token in Postman for the above query. This time, I got the following result.

Cool! It worked then. The ijhar_admin user has the right permission, so, it gave access to the information but the second user ijhar_staff does not have the permission. So, It didn’t give access.

So, our decorator is working perfectly. We can check more than one permission with it like this.

@can("manage_own_profile", "manage_authors_profile")

If you would like to check out the full source code, check it out from my repo.