'Add security checks in graphene resolver

I am using Django and Graphene to serve a graphql endpoint and I have hit a bit of a problem I can't seem to figure out.

I have following resolver:

class Query(ObjectType):
    trainingSession = Field(TrainingSessionType, id=graphene.ID())
    trainingSessions =  DjangoFilterConnectionField(TrainingSessionType)

   

    @staticmethod
    def checked_trainingsession(trainingsession,info):
       # returns the trainingsession if a certain logic is fulfilled
       # else None


    def resolve_trainingSessions(root, info,**kwargs):
        ids= kwargs.get('discipline__id')
        all = TrainingSession.objects.all()

        result = []
        for trainingSession in all:
            trainingSession = Query.checked_trainingsession(trainingSession,info)
            if trainingSession != None:
                result.append(trainingSession)
        return result

together with the Objects types and Filters:

class TrainingSessionFilter(FilterSet):
    discipline__id = GlobalIDMultipleChoiceFilter()
    class Meta:
        model = TrainingSession
        fields = ["discipline__id"]

class TrainingSessionType(DjangoObjectType):
    class Meta:
        model=TrainingSession
        fields="__all__"
        filterset_class = TrainingSessionFilter
        interfaces = (CustomNode,)


class CustomNode(graphene.Node):
    """
        For fetching object id instead of Node id
    """
    class Meta:
        name = 'Node'

    @staticmethod
    def to_global_id(type, id):
        return id

however when I try to execute a query


query Sessions{
    trainingSessions(discipline_Id:[2,3]){
    edges{
      node{
        dateTime,
        discipline{
          id
        }
      }
    }
  }
}

I get the Error:

Traceback (most recent call last):
  File "D:\Ben\GitHub-Repos\dojo-manager\env\lib\site-packages\promise\promise.py", line 489, in _resolve_from_executor
    executor(resolve, reject)
  File "D:\Ben\GitHub-Repos\dojo-manager\env\lib\site-packages\promise\promise.py", line 756, in executor
    return resolve(f(*args, **kwargs))
  File "D:\Ben\GitHub-Repos\dojo-manager\env\lib\site-packages\graphql\execution\middleware.py", line 75, in make_it_promise
    return next(*args, **kwargs)
  File "D:\Ben\GitHub-Repos\dojo-manager\env\lib\site-packages\graphene_django\fields.py", line 176, in connection_resolver
    iterable = queryset_resolver(connection, iterable, info, args)
  File "D:\Ben\GitHub-Repos\dojo-manager\env\lib\site-packages\graphene_django\filter\fields.py", line 62, in resolve_queryset
    return filterset_class(data=filter_kwargs, queryset=qs, request=info.context).qs
  File "D:\Ben\GitHub-Repos\dojo-manager\env\lib\site-packages\django_filters\filterset.py", line 193, in __init__
    model = queryset.model
graphql.error.located_error.GraphQLLocatedError: 'list' object has no attribute 'model'

I know i should be returning a queryset from resolve_trainingSessions. However, I don't know how to then apply my permission checks on the individual results. The logic is not super complicated, but I can't really wrap it in to a standard Django model filter or Q object.

Thanks for any help or hints.



Solution 1:[1]

Ok I managed to solve my issue by following this Idea: https://docs.graphene-python.org/projects/django/en/latest/authorization/#user-based-queryset-filtering

if user.is_anonymous:
    return TrainingSession.objects.none()

if user.is_superuser:
    return TrainingSession.objects.filter(filter)
....

Not super elegant but it does its job and it's not too bad.

Solution 2:[2]

so as you may have guessed by now, the reason that queries with DjangoFilterConnectionField types has to return a queryset instead of list is so that the pagination works properly, which comes out of the box with it. Unfortunately for users who only care about filtering but not pagination, opting out of returning a queryset is not really possible. So you have three options. (PS I have made some slight other changes into your code snippets, (e.g using @classmethod)

  1. You don't use a DjangoFilterConnectionField
class Query(ObjectType):
    trainingSession = Field(TrainingSessionType, id=graphene.ID())
    trainingSessions =  graphene.List(TrainingSessionType, discipline__id=grapene.List(graphene.ID)

   
    @staticmethod
    def checked_trainingsession(trainingsession,info):
       # returns the trainingsession if a certain logic is fulfilled
       # else None

    @classmethod
    def resolve_trainingSessions(cls, info, discipline__id):
        return [ts for ts in TrainingSession.objects.filter(id__in=discipline_id) if cls.checked_trainingsession(ts, info)]

The advantage of this method is that in the case that your queryset has some items you are allowed to see, then you can still return them, without having to return a queryset object (you can do non-database filtering).

  1. You try really hard to write your checked_trainigsession as a queryset filter - it might seem impossible but after enough sweat you might be able to pull it off.. ive been there.

  2. As you've done here, you sacrifice your ability to return partial data if the user is only allowed to see some items, and just raise an error when it doesn't happen. The way you return queryset.objects.none() is fine, but you can just as easily raise an error (which could be more elegant?)

from graphql_jwt.decorators import superuser_required

class Query(ObjectType):
    trainingSession = Field(TrainingSessionType, id=graphene.ID())
    trainingSessions =  DjangoFilterConnectionField(TrainingSessionType)

    @superuser_required
    def resolve_trainingSessions(root, info,**filters):
        return TrainingSessionFilter(filters).qs

You can also swap out superuser_required for login_required or staff_member_required. Good luck!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 bwright
Solution 2 rymanso