5

I have seen a number of discussions on how to implement a permission system in graphene, but have not seen any definitive practical outcome beyond these discussions. Some examples of discussions on this topic are:

Unfortunately, none of these recommend a preferred approach to implementing permissions in graphene. Does anyone know what is currently the best practice for doing this?

martasd
  • 111
  • 1
  • 1
  • 8

1 Answers1

3

First of all splitting API endpoints into public/private make sense only when public interface differs significantly from private one. If not then you will face redundancy in code problem.

In our project we came up with simple solution which seems to be pointed by many as desired solution.

We use the following decorator on resolve methods:

# decorators.py

def permission_required(permission):
    """ Checking permissions on per method basis. """

    def wrapped_decorator(func):
        def inner(cls, info, *args, **kwargs):
            if check_permission(permission, info.context):
                return func(cls, info, **kwargs)
            raise Exception("Permission Denied.")

        return inner

    return wrapped_decorator


def check_permission(permission, context):
    """
    Helper function to resolve permissions.
    Permission can be a string "app_name.perm_codename"
    or callable (lambda) function with user passed as an argument:
    example: lambda(user): user.username.startswith('a')
    """

    if callable(permission):
        if not permission(context.user):
            return False
    else:
        if not context.user.has_perm(permission):
            return False
    return True

You can use this decorator as follows:

# schema.py

from . decorators import permission_required

class UserNode(DjangoObjectType):

    class Meta:
        model = User
        interfaces = (relay.Node,)
        only_fields = (
            'id', 'first_name', 'last_name',
            'email', 'username'
        )
        filter_fields = {
            'username': ['exact'],
            'id': ['exact'],
        }

    role = graphene.String(description="User's role in the system.")

    @permission_required('our_app.some_perm')
    def resolve_role(self, info, **kwargs):
        if info.context.user.username in ['dev1', 'dev2']:
            return "developer"
        if info.context.user.is_superuser:
            return "admin"
        if info.context.user.is_staff:
            return "staff"
        return "guest"

If you don't have this particular permission our_app.some_perm you'll get the following response:

{
  "errors": [
    {
      "message": "Permission Denied.",
      "locations": [
        {
          "line": 7,
          "column": 9
        }
      ],
      "path": [
        "userSet",
        "edges",
        0,
        "node",
        "role"
      ]
    },
    {
      "message": "Permission Denied.",
      "locations": [
        {
          "line": 7,
          "column": 9
        }
      ],
      "path": [
        "userSet",
        "edges",
        1,
        "node",
        "role"
      ]
    }
  ],
  "data": {
    "userSet": {
      "edges": [
        {
          "node": {
            "id": "VXNlck5vZGU6MQ==",
            "username": "user1",
            "role": null
          }
        },
        {
          "node": {
            "id": "VXNlck5vZGU6Mg==",
            "username": "user2",
            "role": null
          }
        }
      ]
    }
  }
}

When you need more expressive way to check permission, for example when checking multiple permissions with or statement, use lambdas in @required_permission decorator:


@permission_required(lambda u: u.has_perm('app.perm1') or u.has_perm('app.perm2'))
def resolve_something1(self, info, **kwargs):
    # ... do your stuff here
    return data

@permission_required(lambda user: user.username.startswith('a'))
def resolve_something2(self, info, **kwargs):
    # ... do your stuff here
    return data

robertu
  • 116
  • 1
  • 7