Skip to main content

graphene-django-permissions

pypi python Build Status codecov

A performant, holistic view-permissions layer for graphene / graphene-django, which augments the python GraphQL API using Django's built-in permissioning system, such that it only returns models that the user is authorized to see, regardless of how their query or mutation is formed.

Installation

pip install graphene-django-permissions

Usage

In your Django settings.py file, update your Graphene configuration to include the authorization middleware:

GRAPHENE = {
    "SCHEMA": "path.to.schema.schema",
    "MIDDLEWARE": (
        "graphene_django_permissions.middleware.GrapheneAuthorizationMiddleware",
    ),
}

And you're all set!

At this point, Graphene/GraphQL will only return model data that users are permitted to see, based on their Django model-level view permissions (like polls.view_poll for returning Poll model objects).

If a user (or a group the user is in) is granted the view permissions to a model (e.g. via user.user_permissions.add(), such that user.has_perm("polls.view_poll") returns True), Graphene will continue returning all instances of that model in its query and mutation responses. If the user does not have that view permission generally, the authorization middleware will check that the user has object-level permissions via user.has_perm(), and only return specific instances which the user is allowed to see.

See here for info on Django's default model permissions, and here for info on object permissions. Typically the object-level authorization backend is implemented with an external library, like the popular django-guardian or django-rules packages.

Requirements

Compatibility

graphene-django-permissions graphene-django
1.0.0+ v3.0.2+
0.1.0 v2

Motivation

The power of GraphQL is that the client can ask for exactly what data they need. But with that capability comes a risk: the backend needs to ensure that no matter what fields the client requests, the API only returns data they're authorized to see.

For example, suppose you have a Django models as follows:

class Expense(models.Model):
    creator = models.ForeignKey(User, related_name="expenses")
    amount = models.IntegerField()

And a corresponding Graphene/GraphQL schema like:

class Expense(DjangoObjectType):
    class Meta:
        model = models.Expense

class User(DjangoObjectType):
    class Meta:
        model = models.User

class Query(graphene.ObjectType):
    user = graphene.Field(User, id=graphene.ID())
    expenses = graphene.List(graphene.NonNull(Expense), required=True)

While we could update our resolve_expenses method so that we only allow users to load expenses in that query if they have permission, this would be an incomplete solution. A user could still form a separate query like query { user(id:42) { id, expenses { ... } } } to "indirectly" gain access to expenses via that alternative entry-point, where the expenses resolver would not come into play. These relationships will exist throughout a GQL application via numerous models and arbitrarily deep nesting, so trying to perform authorization with resolvers alone will undoubtedly spell trouble.

Instead, we'd like to restrict viewing models no matter what query pattern the client uses, which is what graphene-django-permissions allows. Whether you need model-level or object-level permissioning, you can be sure that the logic is applied everywhere you attempt to return Django models.

This was originally inspired by the popular JS library, graphql-shield, which uses a middleware-based approach for GraphQL authorization.

What is this not?

This library/middleware is not used for restricting access to route-level checks (i.e., individual queries or mutations). Instead, it is designed to ensure that no matter which query or mutation is used, the data returned to the user only includes models they're authorized to see.

To apply permissioning to a particular query or mutation (for instance, to only allow certain users to mutate some a model), you can use standard Graphene/python logic, like:

class UpdateUser(graphene.Mutation):
    class Arguments:
        user_id = graphene.Int()

    @classmethod
    def mutate(cls, root, info, user_id):
        if info.context.user.id != user_id:
            raise Exception("You do not have permission to perform this action")
        ...

or use an approach like decorators from django-graphql-jwt, or the mutation permissions field in graphene-django-cud. These options (and the above code example with custom logic) are all complementary to graphene-django-permissions, since even if you restrict access for a user to perform a given query or mutation, you still want to be confident that you only return data to a user if they're authorized to see it (no matter which fields they request in their query/mutation).

Complementary/recommended projects

  • graphene-django-optimizer: Essential for performant Django model-based graphql.
  • graphene-django-cud: Highly recommended for dramatically reducing boilerplate in defining create/update/delete mutations, including specifying permissions for accessing them.

Alternatives

There are a few alternative ways one could apply authorization logic with Graphene, as alluded to above, though they have some shortcomings that tend to make a middleware-approach like graphene-django-permissions a better option.

Filtering at the ObjectType level

The official graphene-django docs recommend recommend adding logic like:

class PostNode(DjangoObjectType):
    class Meta:
        model = Post

    @classmethod
    def get_queryset(cls, queryset, info):
        if info.context.user.is_anonymous:
            return queryset.filter(published=True)
        return queryset

While this functionally might accomplish what you need (granted, you have to take care to ensure get_queryset is respected in all of your access patterns), it ends up hurting SQL performance dramatically if you're relying on a tool like graphene-django-optimizer (which you should!). This is because if the Post model ends up being queried via some nested pattern (e.g. you fetch a list of users, and the Posts of every user), the queryset.filter() call in the example above will end up causing an N+1 query pattern, since it will issue a new query per nested "posts" list.

graphene-django-permissions avoids this problem by filtering in-memory, after the SQL queries have been performed, so the query patterns are not directly affected by authorization logic. While it would be nice/preferable to more deeply integrate into the SQL query generation to avoid fetching the non-permitted data in the first place, along the lines of what's possible in SQLAlchemy (like with sqlalchemy-oso), this is seemingly much trickier to do with Django and GQL in a consistent and performant way. (If you have any ideas on how to achieve something like this, please suggest it!)

Other libraries

There are a few other libraries (graphene-permissions, django-graphene-permissions, graphene-field-permission) that support authorization/permissions, but they seem to share similar limitations in that (1) they do not support object-level permissions in a reasonable/performant way (e.g. see this issue), and (2) they require every model ObjectType to be updated individually to apply permissioning.

Metadata

Release files for graphene-django-permissions 1.0.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-django-permissions 1.0.0
File Size Uploaded
graphene_django_permissions-1.0.0.tar.gz 19.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for graphene-django-permissions 1.0.0
File Interpreter ABI Platform
graphene_django_permissions-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 26.8 kB

Release files / graphene_django_permissions-1.0.0.tar.gz

Download URL graphene_django_permissions-1.0.0.tar.gz
Size 19.4 kB
Tags Source
SHA-256 checksum
How to use checksums
581976096eb84290ccdd5f788e1fc09055d47a782032cab934b0b9f6fa3180e5
BLAKE2b-256 checksum
How to use checksums
839aa1cac1c89493bb19763ced50ba2a2eb942f97506fe5684e196ef3238b10b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.11.3

Release files / graphene_django_permissions-1.0.0-py3-none-any.whl

Download URL graphene_django_permissions-1.0.0-py3-none-any.whl
Size 7.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
42f93e5fbe67fc55094f10ac5f08d1066af7b3aa60564c9ef7f8d5b4a961d4be
BLAKE2b-256 checksum
How to use checksums
f729d9a3c22f8545f19dc7ba28c771b1136ea7b4d1e2c281363d1acefbeadcf5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.11.3

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.1.0

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