5

I have read about how to exclude (hide) certain fields in django and graphene_django in these Links:

Imagine that we have the following Post model which has foreign key to the User model.

apps/posts/models.py

from django.db import models
from apps.users.models import User

class Post(models.Model):
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        null=False
    )

    created_at = models.DateTimeField(
        auto_now_add=True,
    )

    title = models.TextField(
        null=False,
        max_length=100,
    )

    content = models.TextField(
        null=False,
    )

    def __str__(self):
        return self.title

apps/users/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser, models.Model):
    phone_no = models.CharField(
        blank=True,
        null=True,
        default="",
        max_length=10,
        verbose_name="Phone Number",
    )

    avatar = models.ImageField(
        null=True,
        upload_to='static',
    )

    USERNAME_FIELD = "username"
    EMAIL_FIELD = "email"

    def __str__(self):
        return self.username

I tried the following but it doesn't work as expected:

apps/posts/schema.py

import graphene
from graphene import Mutation, InputObjectType, ObjectType
from graphene_django.types import DjangoObjectType

from .models import Post


class PostType(DjangoObjectType):
    class Meta:
        model = Post
        exclude_fields = [
            'created_at', #it worked
            'author.password', #the way I tried to hide the foreign key field
            
        ]

class Query(ObjectType):
    posts = graphene.List(
        PostType
    )

    def resolve_posts(self, info):
        #TODO: pagination
        return Post.objects.all()

Screenshot:

insomnia screenshot

How can I hide certain fields of it (like author's password in the above example) in graphql model Types?

JPG
  • 82,442
  • 19
  • 127
  • 206
Mostafa Ghadimi
  • 5,883
  • 8
  • 64
  • 102

3 Answers3

4

From the comments, I have understood that you have a UserType class

You can use the exclude option in the meta as,

class UserType(DjangoObjectType):
    class Meta:
        model = User
        exclude = ('password',)

Update

You can also use a custom resolver to check the requested entity

class UserType(DjangoObjectType):
    password = graphene.String()

    def resolve_password(self, info):
        requested_user = info.context.user
        if requested_user.email in ['admin@test.com', 'ceo@test.com']:
            return self.password
        return None

    class Meta:
        model = User
        fields = '__all__'
JPG
  • 82,442
  • 19
  • 127
  • 206
3

For the use case that I see being answered often, which is for conditionally hiding fields, the solution that suited me the best if you have many fields, is by using middleware: https://docs.graphene-python.org/en/latest/execution/middleware/#example

For example:

Inside a file called app.graphene_senstitive_fields.py:

from django.contrib.auth.models import User
from graphql import GraphQLError

SENSITIVE_FIELDS = ("password", "streetAddress") # camelCase, as converted by graphene


class SensitiveFieldMiddleware:
    def resolve(self, next, root, info, **args):
        if (
            type(root) is User
            and root != info.context.user
            and info.field_name in SENSITIVE_FIELDS
        ):
            raise GraphQLError(
                f"`{info.field_name}` for {root.user} cannot be viewed by you as it is "
                "a sensitive field and you are not the owner!"
            )
        return next(root, info, **args)

Inside of settings.py:

GRAPHENE = {
    "SCHEMA": ...,
    "MIDDLEWARE": ["app.graphene_sensitive_fields.SensitiveFieldMiddleware"],
}

The advantage of this solution is that it's easier to hide multiple fields (my lists are much larger) without having to write a resolve method for each one.

Another advantage is that this raises an explicit exception for that field, and returns a null for it, without breaking the rest of the query (valid info still gets through).

A downside is if you have many different models to protect, then you would have to refactor the code a bit to keep it manageable and avoid a long chain of if statements.

Another downside that I don't have a solution for right now is that you need to list the names of the fields as they are presented to the end user; in camelCase. I don't know how to get to the original field name.

2

This is answer only for checking permissions before resolving query field. (So, not an answer to original question)

Something like this would work.


def permission_check_my_field(func):
    @wraps(func)
    def wrapper(self,info,**kwargs):
        user=info.context.user
        if (......) # permit condition here
           return func(self, info,**kwargs)
        else:
            return None
    return wrapper

class  Query(graphene.ObjectType):
    my_field = graphene.Field(...) # or graphene.List or .....
    
    @permission_check_my_field
    def resolve_my_field(......)
      # do your normal work

Update.

Above code works if the user data is enough to check if the field is accessible (like the other answer). However, if you need to check whether or not user has been granted some permission to access that field, then you need to do like this:


def permission_check_in_query(perm):
    def wrapped_decorator(func):
        @wraps(func)
        def wrapper(self,info,**kwargs):
            user=info.context.user
            if user.has_perm(perm) # check if the user has privilege
                return func(self, info,**kwargs)
            else:
                return None 
                # All fields are not nullable, so  `return None' might throw error. You can pass `what you need to return` in decorator argument and use it here , to avoid this.   
        return wrapper
    return wrapped_decorator

class  Query(graphene.ObjectType):
    my_field = graphene.Field(...) # or graphene.List or .....
    
    @permission_check_in_query('model.access_my_field') # your permission code 
    # learn django permissions if you are not sure what it is
    # doesn't have to be django_permission, can be any argument like 'is_it_a_superuser' that you will use to check user privilege. Modify decorator code accordingly
    def resolve_my_field(......)
      # do your normal work

Doing this, you can reuse for any field and any permission. Just add the decorator @permission_check_in_query(your arguments) above any field that needs to be permission-checked before being resolved.

TLDR: This answer is similar to other answer regarding the type of data that API accepts and returns. It just offers reusability and permission check.

Sagar Adhikari
  • 1,312
  • 1
  • 10
  • 17
  • I have added bounties for this question, I would be thankful if you could guide me what to do and update your answer. – Mostafa Ghadimi Sep 04 '20 at 23:00
  • This answer I have given is similar to the other answer, except it returns 'PermissionDenied' and I have just added decorators for reusability. You can create a decorator that accepts arguments i.e your permission codes(here I have used argumentless decorator) which will make it reusable for any field. – Sagar Adhikari Sep 05 '20 at 09:26
  • Now, I have updated docorator with argument where you can pass permission and check. – Sagar Adhikari Sep 05 '20 at 09:53
  • 1
    It’s all ok now :) – Mostafa Ghadimi Sep 05 '20 at 12:13