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.

Mark Chackerian
  • 21,866
  • 6
  • 108
  • 99
Berry
  • 2,143
  • 4
  • 23
  • 46
  • Best and simplest suggestion is you need to create a Group then add a custom permissions and include particular members to that group. – Shift 'n Tab Mar 13 '18 at 09:06
  • 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. – Berry Mar 13 '18 at 09:27
  • When you say "reducer" do you mean "reducer" in the React/Redux sense, or do you actually mean "resolver"? – Mark Chackerian Mar 13 '18 at 22:38
  • Yeah, it's resolver, my bad. – Berry Mar 13 '18 at 22:41
  • 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). – Mark Chackerian Mar 14 '18 at 14:47

2 Answers2

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.

Mark Chackerian
  • 21,866
  • 6
  • 108
  • 99
  • 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)? – PKKid May 29 '19 at 17:27
  • @PKKid There seem to be some ways to restrict fields explicitly in the source code https://github.com/graphql-python/graphene-django/blob/master/graphene_django/types.py but they're not documented as far as I know, and they don't allow conditional based access – Mark Chackerian May 29 '19 at 17:55
  • 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. – PKKid May 30 '19 at 01:37
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!')
Lukasz
  • 426
  • 2
  • 13
  • 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. – Mark Chackerian Apr 24 '18 at 14:13