'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)
- 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).
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.
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 |