Package: graphene-django-permissions
graphene-django-permissionsA performant, holistic view-permissions layer forgraphene/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.Installationpipinstallgraphene-django-permissionsUsageIn your Djangosettings.pyfile, update yourGraphene configurationto 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-levelviewpermissions (likepolls.view_pollfor returningPollmodel objects).If a user (or a group the user is in) is granted the view permissions to a model (e.g. viauser.user_permissions.add(), such thatuser.has_perm("polls.view_poll")returnsTrue), Graphene will continue returningallinstances of that model in its query and mutation responses. If the user doesnothave that view permission generally, the authorization middleware will check that the user has object-level permissions viauser.has_perm(), and only return specific instances which the user is allowed to see.Seeherefor info on Django's default model permissions, andherefor info on object permissions. Typically the object-level authorization backend is implemented with an external library, like the populardjango-guardianordjango-rulespackages.Requirementspython(3.7+)graphene-django(see compatibility table below)GrapheneSchemas mustnotuse Relay / Nodes (untilhttps://github.com/sjdemartini/graphene-django-permissions/issues/1is resolved)Compatibilitygraphene-django-permissionsgraphene-django1.0.0+v3.0.2+0.1.0v2MotivationThe 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:classExpense(models.Model):creator=models.ForeignKey(User,related_name="expenses")amount=models.IntegerField()And a corresponding Graphene/GraphQL schema like:classExpense(DjangoObjectType):classMeta:model=models.ExpenseclassUser(DjangoObjectType):classMeta:model=models.UserclassQuery(graphene.ObjectType):user=graphene.Field(User,id=graphene.ID())expenses=graphene.List(graphene.NonNull(Expense),required=True)While we could update ourresolve_expensesmethod so that we only allow users to loadexpensesin that query if they have permission, this would be an incomplete solution. A user could still form a separate query likequery { user(id:42) { id, expenses { ... } } }to "indirectly" gain access to expenses via that alternative entry-point, where theexpensesresolver 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 whatgraphene-django-permissionsallows. 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 isnotused 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:classUpdateUser(graphene.Mutation):classArguments:user_id=graphene.Int()@classmethoddefmutate(cls,root,info,user_id):ifinfo.context.user.id!=user_id:raiseException("You do not have permission to perform this action")...or use an approach like decorators fromdjango-graphql-jwt, or the mutationpermissionsfield ingraphene-django-cud. These options (and the above code example with custom logic) are all complementary tographene-django-permissions, since even if you restrict access for a user toperforma 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 projectsgraphene-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.AlternativesThere 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 likegraphene-django-permissionsa better option.Filtering at theObjectTypelevelThe officialgraphene-django docs recommendrecommend adding logic like:classPostNode(DjangoObjectType):classMeta:model=Post@classmethoddefget_queryset(cls,queryset,info):ifinfo.context.user.is_anonymous:returnqueryset.filter(published=True)returnquerysetWhile this functionally might accomplish what you need (granted, you have to take care to ensureget_querysetis respected in all of your access patterns), it ends up hurting SQL performance dramatically if you're relying on a tool likegraphene-django-optimizer(which you should!). This is because if thePostmodel ends up being queried via some nested pattern (e.g. you fetch a list of users, and thePosts of every user), thequeryset.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-permissionsavoids 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 withsqlalchemy-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 librariesThere 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. seethis issue), and (2) they require every modelObjectTypeto be updated individually to apply permissioning.