Skip to main content

Graphene Field Permission

A package to add field-level permissions for Graphene Python.

Currently only supports Django/Graphene Django but would welcome pull requests adding support for other ORMs.

Usage

Queries

On schema nodes add the decorator @has_field_access to the resolve for any field that you want checked.

Usage example in schema files:

from graphene_field_permission.decorators import has_field_access

class GroupNode(DjangoObjectType):
    @has_field_access('permission1')
    def resolve_group_name(self, info):
        return self.name

    class Meta:
        model = Group
        ...

Example showing checking for one of multiple (unlimited) permissions:

from graphene_field_permission.decorators import has_field_access

class GroupNode(DjangoObjectType):
    @has_field_access('permission1', 'permission2')
    def resolve_group_description(self, info):
        return self.description

Example showing checking for one of multiple permissions under a group, for cases where permissions differ by group id:

from graphene_field_permission.decorators import has_field_access

class GroupNode(DjangoObjectType):
    @has_field_access('permission1', 'permission2', filter_field='group_id')
    def resolve_group_text(self, info):
        return self.text

filter_field

A filter_field value of 'group_id' in the above example will look at the GroupNode group_id field. Add . separators for related objects.

filter_field processing will traverse related objects as necessary to reduce the number of queries.

It's recommended to try and reduce these as much as possible. e.g. using group.division.corporation_id instead of group.division.corporation.id

@has_field_access('permission1', 'permission2', filter_field='group.division.corporation_id')

Mutations

Add check_field_access() call for the permission you want to confirm - one check per mutation will work. Raises PermissionError if no match found. Permission arguments are logical OR.

from graphene_field_permission import check_field_access

class ModifyGroup(graphene.Mutation):
     # Normal mutation setup here...

    def mutate(self, info, id, field_1_data, field_2_data):
        d = ORM.objects.find(pk=id)
        d.field_1 = field_1_data
        # checking of permissions:
        try:
            check_field_access('permission1', info_context=info.context)
            d.protected_field = field_2_data
        except PermissionError as exc1:
            raise Exception from exc1        
        d.save()

Grouped permissions

from graphene_field_permission import check_field_access

class ModifyGroup(graphene.Mutation):
     # Normal mutation setup here...

    def mutate(self, info, id, field_1_data, field_2_data):
        fd = ModelName.objects.find(pk=id)
        fd.field_1 = field_1_data
        # checking of permissions:
        try:
            # check for user having permission1 OR permission2
            check_field_access(
                'permission1',
                'permission2',
                filter_field='group.division.corporation_id',
                filter_data=fd,
                info_context=info.context,                
            )
            fd.protected_field = field_2_data
        except PermissionError as exc1:
            raise Exception from exc1        
        fd.save()

filter_field along with filter_data works the same way as filter_field does in queries, with filter_data providing the ORM object to be traversed.

More than one check_field_access() call can be made and retrieved permissions will be retained between the calls.

Sample Result in GraphQL output from query decorator:

{
  "errors": [
    {
      "message": "No access for user on field 'group_name'",
      "locations": [
        {
          "line": 7,
          "column": 9
        }
      ],
      "path": [
        "group",
        "edges",
        0,
        "node",
        "groupName"
      ]
    }
  ],
  "data": {
    "group": {
      "edges": [
        {
          "node": null
        }
      ]
    }
  }
}

Usage notes:

  1. An exception is thrown should a user attempt to access a field for which they don't have access. the reason for this is that graphene-django doesn't allow returning None for fields which aren't set as nullable so this is the best way of proceeding and follows that convention throughout. That makes it necessary to have your graphql queries fine grained enough to not call those fields in the first place.
    1. Client side checking of permissions is recommended in order to limit the field's accessed in the query in the first place.

Installation

pip install graphene-field-permission

Setup

  1. Set up graphene and graphene django following their own instructions.
  2. Create a module and method that will return permissions allowed for the user as shown below. By default lists and dicts containing lists are supported.
  3. Update settings.py to match the instructions below.

Example permissions resolution

Standard:

def get_user_permissions(user):
    # query database to determine the passed in user's permissions
    return ['permission1', 'permission2', 'permission3']

Or grouped:

def get_user_permissions(user):
    # query database to determine the passed in user's permissions
    return {
        'group-id-123': ['permission1', 'permission2', 'permission3'],
        'group-id-456': ['permission1', 'permission3', 'permission5'],
    }

User Permission Call Information

  1. These get called once per graphql query call.
  2. It's recommended to use the logs to try to minimise the number of queries generated by this function. Ideally it would be best to do it in a single query.
  3. It's recommended to use Django ORM's select_related on queries where necessary in order to minimise the number of queries.

Settings

With the above method at app/helpers/user_permissions.py (for example) update settings.py to add:

GRAPHENE_FIELD_PERMISSION = {
    'SRC_MODULE': 'app.helpers.user_permissions',
    'SRC_METHOD': 'get_user_permissions',
}

Also update the main graphene settings to add the middleware.

GRAPHENE = {
    'MIDDLEWARE': [
        'graphene_field_permission.permissions.PermissionsMiddleware'
    ]
}

Unit testing against schemas using Graphene Field Permission

To have pytest override checks in schema unit tests you can use the graphene_field_permissions_allowed fixture to have check_field_access and has_field_access resolve as if the user has permissions.

import pytest
import schema from your_app.schema
from graphene_field_permission.fixtures import graphene_field_permissions_allowed


class TestSample:
    @pytest.fixture(autouse=True)
    def setup(self):
        self.client = Client(schema=your_schema)
        # ... insert models setup here ...

    def test_query(self, graphene_field_permissions_allowed):
        your_query = """
        ... insert your query here ...
        """
        your_response = self.client.execute(your_query, variables={})
        assert set_status_response == None # modify None to match your expected data

Future updates, design notes

  1. I don't plan to develop this a whole lot further. It has scratched my itch for now.
  2. I tried about four different ways to do this so resolve_field wasn't necessary, but found this to be the best balance of making it schema-definable and performant. I'm open to pull requests if someone can think of a better way.
  3. This currently only supports Graphene under Django. I'm open to others adding support for other graphene-python projects if they want to submit pull requests.

Release files for graphene-field-permission 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for graphene-field-permission 1.1.0
File Size Uploaded
graphene-field-permission-1.1.0.tar.gz 12.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for graphene-field-permission 1.1.0
File Interpreter ABI Platform
graphene_field_permission-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 27.5 kB

Release files / graphene-field-permission-1.1.0.tar.gz

Download URL graphene-field-permission-1.1.0.tar.gz
Size 12.9 kB
Tags Source
SHA-256 checksum
How to use checksums
22c75a993d03e366a4bbb95a8fc783b16eb8d999b091cf03c93b92fb201848d0
BLAKE2b-256 checksum
How to use checksums
c01e499a2604931b8426acb043be3f1af1cfe08211e1f296b767f4ce853b1b00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.0 CPython/3.9.7

Release files / graphene_field_permission-1.1.0-py3-none-any.whl

Download URL graphene_field_permission-1.1.0-py3-none-any.whl
Size 14.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2fda0506d230ab296d45448621de6169062472abfe8fb025e5cb7210a740019e
BLAKE2b-256 checksum
How to use checksums
fc87a92ed439752b57ee93fc21a49952eaa0c195b31cb6078eba8cb1a995d1aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.0 CPython/3.9.7

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.0

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.3

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page