How do you prevent manual GraphQL traversals to sensitive data? How do you prevent accidental exposure of data to the wrong people?

Making sure the right people have access to your data is crucial in this day and age. We use Django and Graphene on our backend to deliver content via GraphQL. This exposed some problems, in traditional REST (or django views or DRF) you would lock down endpoints and serve only what is required but with GraphQL the frontend decides what it needs. This flexible layer is great for development but not great for security.

Using Decorators

The easy approach is to decorate any resolvers in Graphene.

@only_roles('DOCTOR', 'NURSE')
def resolve_appointments(context):
    return Appointment.objects.all()

However, what if doctors can only access appointments in their state? I’ll simplify the access pattern.

@only_roles('DOCTOR', 'NURSE')
def resolve_appointments(context):
    return Appointment.objects.filter(state=context.user.state)

Okay we have now protected our appointments resolver… in one place. A few months from then a developer comes along and adds a resolver for an individual appointment.

@only_roles('DOCTOR', 'NURSE')
def resolve_appointments(context):
    return Appointment.objects.filter(state=context.user.state)

@only_roles('DOCTOR', 'NURSE')
def resolve_appointment(context, id):
    return Appointment.objects.get(id=id)

Our doctor and nurse roles can now access any appointment provided they know the identifier.

Using the ORM

Django exposes a few functions that we can play with to override resulting sets. Overriding at this lower level gives us a few benefits:

  • Assurance against forgetting to annotate, people make mistakes
  • You’re protected regardless of how the objects are accessed, e.g. the admin panel, REST endpoints

How does it work?

We use the django-crequest library which gives us access to the requesting user which is abstracted away by the time you get to the ORM. We then override get_queryset to inject our own filtering logic before the ORM can return objects.

from crequest.middleware import CrequestMiddleware

class PermissionManager(models.Manager):
    def get_queryset(self):
        results = super().get_queryset()
        current_request = CrequestMiddleware.get_request()
        current_user = getattr(current_request, "user", None)
        # Apply any filtering logic here using results + current_user
        return filtered_results


class Appointment(models.Model):
    objects = PermissionManager()
    ...

You can of course create an abstraction/interface class now that implements permissions depending on your underlying schema. For example you may have a state, system or permission assigned to models regardless of their content_type.

Conclusion

There are probably hundreds of ways to do the same thing, let me know if you have a better approach or if there are gaps in ours!