How to limit field access on a model based on user type on Graphene/Django?
Asked Answered
D

2

18

Let's say I have a model:

class Employee(models.Model):
    first_name = models.CharField(max_length=40)
    last_name = models.CharField(max_length=60)
    salary = models.DecimalField(decimal_places=2)

I want anyone to be able to access first_name and last_name but only want certain users to be able to read salary because this is confidential data.

And then I want to restrict write/update for salary to an even different kind of user.

How do I restrict field read/write/update depending on the request user?

EDIT:

This is in the GraphQL API context. I am using Graphene. I'd like to see a scalable solution in the resolver function.

Developer answered 3/3, 2018 at 12:53 Comment(5)
Best and simplest suggestion is you need to create a Group then add a custom permissions and include particular members to that group.Sherburne
I want to see how you access the request from the reducer, how you send an error on a query with unauthorized fields, how to process the authentication in the reducer. The problem is more about the GraphQL integration of the authentication instead of the Django end of it.Developer
When you say "reducer" do you mean "reducer" in the React/Redux sense, or do you actually mean "resolver"?Monoplegia
Yeah, it's resolver, my bad.Developer
There's two parts to this question, because the graphene-python implementation requires separate functionality for READING (i.e. queries) vs. WRITING (i.e. mutations).Monoplegia
M
20

QUERIES

Assuming that you have

  1. a query defined like
    employees = graphene.List(EmployeeType)
  1. a resolver for the query like
    def resolve_employees(self, info, **kwargs):
        return Employee.objects.all()

and

  1. permissions on your Employee model called can_view_salary and can_edit_salary

Then you'll need to define the EmployeeType with a value of salary that is dependent on the user. Something like

from graphene_django.types import DjangoObjectType
from myapp.models import Employee

class EmployeeType(DjangoObjectType):
    class Meta:
        model = Employee
        
    def resolve_salary(self, info):
        if info.context.user.has_perm('myapp.can_view_salary'):
            return self.salary
        return None

The important takeaway is that you're creating a custom resolve function for the salary that is switching based on the value of a permission. You don't need to create any other resolvers for first_name and last_name.




MUTATIONS

Read the documentation first. But there isn't an example for doing an update.

In brief, here's the approach that you can take:

  1. Create a method to set the employee in your Mutation method
class MyMutations(graphene.ObjectType):
     set_employee = SetEmployee.Field()
  1. Create a method for SetEmployee that gets the Employee object and updates it. The salary field is ignored for certain users.
class SetEmployee(graphene.Mutation):
    
    class Arguments:
        id = graphene.ID()
        first_name = graphene.String()
        last_name = graphene.String()
        salary = graphene.String()
    
    employee = graphene.Field(lambda: EmployeeType)
    
    
    @classmethod
    def mutate(cls, root, info, **args):
        employee_id = args.get('employee_id')
        
        # Fetch the employee object by id
        employee = Employee.objects.get(id=employee_id)
        first_name = args.get('first_name')
        last_name = args.get('last_name')
        salary = args.get('salary')
        
        # Update the employee fields from the mutation inputs
        if first_name:
            employee.first_name = first_name
        if last_name:
            employee.last_name = last_name
        if salary and info.context.user.has_perm('myapp.can_edit_salary'):
            employee.salary = salary
        employee.save()
        return SetEmployee(employee=employee)

Note: when this answer was originally written, there was no Decimal field available in Graphene Django -- I avoided this issue by taking a string as an input.

Monoplegia answered 14/3, 2018 at 16:33 Comment(3)
This seems counter-intuitive to me. By defining functions like resolve_salary() above, we are essentially creating a black list of fields users are not allowed to view. This could be troublesome in the future when a new privileged field is added to the Employee model and the developer inevitably forgets to also create the resolve_newfield() function to limit it's access. Thus creating a security hole by forgetting to define a function. -- Is there a way to block all fields unless we explicitly allow it (rather than explicitly block it)?Sofar
@Sofar There seem to be some ways to restrict fields explicitly in the source code github.com/graphql-python/graphene-django/blob/master/… but they're not documented as far as I know, and they don't allow conditional based accessMonoplegia
Thanks for that. It's not officially documented, but this discussion in the bugs talks about a the only_fields being referenced in the query. The discussion alone makes me feel a little better about using it ahead of time.Sofar
P
3

Great response @MarkChackerian. However personally, I believe that returning a null value for a field on unauthorised access can be ambiguous, so I personally raise an exception from resolve method like that:

class UnauthorisedAccessError(GraphQLError):
    def __init__(self, message, *args, **kwargs):
        super(UnauthorisedAccessError, self).__init__(message, *args, **kwargs)

def resolve_salary(self, info):
        if info.context.user.has_perm('myapp.can_view_salary'):
            return self.salary
        raise UnauthorisedAccessError(message='No permissions to see the salary!')
Principe answered 23/4, 2018 at 13:56 Comment(1)
It depends on the use case. If the users are writing their own queries, then it's better to raise an exception, as you have shown. However, I think the more common use case is that the graphQL query is embedded in javascript, in which case it's much more practical to have a single graphQL query that works for both categories of users (users with access, and users without access). If you raise an exception, you can't have a single query that works for both kinds of users.Monoplegia

© 2022 - 2024 — McMap. All rights reserved.