Package: graphene-field-permission
Graphene Field PermissionA package to add field-level permissions forGraphene Python.Currently only supports Django/Graphene Djangobut would welcome pull requests adding support for other ORMs.UsageQueriesOn schema nodes add the decorator@has_field_accessto the resolve for any field that you want checked.Usage example in schema files:fromgraphene_field_permission.decoratorsimporthas_field_accessclassGroupNode(DjangoObjectType):@has_field_access('permission1')defresolve_group_name(self,info):returnself.nameclassMeta:model=Group...Example showing checking for one of multiple (unlimited) permissions:fromgraphene_field_permission.decoratorsimporthas_field_accessclassGroupNode(DjangoObjectType):@has_field_access('permission1','permission2')defresolve_group_description(self,info):returnself.descriptionExample showing checking for one of multiple permissions under a group, for cases where permissions differ by group id:fromgraphene_field_permission.decoratorsimporthas_field_accessclassGroupNode(DjangoObjectType):@has_field_access('permission1','permission2',filter_field='group_id')defresolve_group_text(self,info):returnself.textfilter_fieldAfilter_fieldvalue of 'group_id' in the above example will look at the GroupNode group_id field. Add.separators for related objects.filter_fieldprocessing 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')MutationsAddcheck_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.fromgraphene_field_permissionimportcheck_field_accessclassModifyGroup(graphene.Mutation):# Normal mutation setup here...defmutate(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_dataexceptPermissionErrorasexc1:raiseExceptionfromexc1d.save()Grouped permissionsfromgraphene_field_permissionimportcheck_field_accessclassModifyGroup(graphene.Mutation):# Normal mutation setup here...defmutate(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 permission2check_field_access('permission1','permission2',filter_field='group.division.corporation_id',filter_data=fd,info_context=info.context,)fd.protected_field=field_2_dataexceptPermissionErrorasexc1:raiseExceptionfromexc1fd.save()filter_fieldalong withfilter_dataworks the same way asfilter_fielddoes in queries, withfilter_dataproviding the ORM object to be traversed.More than onecheck_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: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 returningNonefor 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.Client side checking of permissions is recommended in order to limit the field's accessed in the query in the first place.Installationpip install graphene-field-permissionSetupSet up graphene and graphene django following their own instructions.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.Update settings.py to match the instructions below.Example permissions resolutionStandard:defget_user_permissions(user):# query database to determine the passed in user's permissionsreturn['permission1','permission2','permission3']Or grouped:defget_user_permissions(user):# query database to determine the passed in user's permissionsreturn{'group-id-123':['permission1','permission2','permission3'],'group-id-456':['permission1','permission3','permission5'],}User Permission Call InformationThese get called once per graphql query call.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.It's recommended to use Django ORM'sselect_relatedon queries where necessary in order to minimise the number of queries.SettingsWith 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 PermissionTo have pytest override checks in schema unit tests you can use thegraphene_field_permissions_allowedfixture to havecheck_field_accessandhas_field_accessresolve as if the user has permissions.importpytestimportschemafromyour_app.schemafromgraphene_field_permission.fixturesimportgraphene_field_permissions_allowedclassTestSample:@pytest.fixture(autouse=True)defsetup(self):self.client=Client(schema=your_schema)# ... insert models setup here ...deftest_query(self,graphene_field_permissions_allowed):your_query="""... insert your query here ..."""your_response=self.client.execute(your_query,variables={})assertset_status_response==None# modify None to match your expected dataFuture updates, design notesI don't plan to develop this a whole lot further. It has scratched my itch for now.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.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.