Merge remote-tracking branch 'origin/master' into convert-form-choice

This commit is contained in:
Olivia Rodriguez Valdes 2019-03-01 16:45:04 -05:00
commit 0541b713e9
5 changed files with 209 additions and 11 deletions

View File

@ -154,18 +154,18 @@ class DjangoConnectionField(ConnectionField):
)
class PermissionField(Field):
class DjangoField(Field):
"""Class to manage permission for fields"""
def __init__(self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs):
"""Get permissions to access a field"""
super(PermissionField, self).__init__(type, *args, **kwargs)
super(DjangoField, self).__init__(type, *args, **kwargs)
self.permissions = permissions
self.permissions_resolver = permissions_resolver
def get_resolver(self, parent_resolver):
"""Intercept resolver to analyse permissions"""
parent_resolver = super(PermissionField, self).get_resolver(parent_resolver)
parent_resolver = super(DjangoField, self).get_resolver(parent_resolver)
if self.permissions:
return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, True)
return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None, None, True)
return parent_resolver

View File

@ -1,6 +1,6 @@
from unittest import TestCase
from django.core.exceptions import PermissionDenied
from graphene_django.fields import PermissionField
from graphene_django.fields import DjangoField
class MyInstance(object):
@ -14,7 +14,7 @@ class PermissionFieldTests(TestCase):
def test_permission_field(self):
MyType = object()
field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver')
field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver')
resolver = field.get_resolver(None)
class Viewer(object):
@ -30,7 +30,7 @@ class PermissionFieldTests(TestCase):
def test_permission_field_without_permission(self):
MyType = object()
field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver')
field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver')
resolver = field.get_resolver(field.resolver)
class Viewer(object):

View File

@ -1,6 +1,6 @@
from mock import patch
from graphene import Interface, ObjectType, Schema, Connection, String
from graphene import Interface, ObjectType, Schema, Connection, String, Field
from graphene.relay import Node
from .. import registry
@ -224,3 +224,101 @@ def test_django_objecttype_exclude_fields():
fields = list(Reporter._meta.fields.keys())
assert "email" not in fields
def extra_field_resolver(root, info, **kwargs):
return 'extra field'
class PermissionArticle(DjangoObjectType):
"""Basic Type to test"""
class Meta(object):
"""Meta Class"""
field_to_permission = {
'headline': ('content_type.permission1',),
'pub_date': ('content_type.permission2',)
}
permission_to_field = {
'content_type.permission3': ('headline', 'reporter', 'extra_field',)
}
model = ArticleModel
extra_field = Field(String, resolver=extra_field_resolver)
def resolve_headline(self, info, **kwargs):
return 'headline'
def test_django_permissions():
expected = {
'headline': ('content_type.permission1', 'content_type.permission3'),
'pub_date': ('content_type.permission2',),
'reporter': ('content_type.permission3',),
'extra_field': ('content_type.permission3',),
}
assert PermissionArticle.field_permissions == expected
def test_permission_resolver():
MyType = object()
class Viewer(object):
def has_perm(self, perm):
return perm == 'content_type.permission3'
class Info(object):
class Context(object):
user = Viewer()
context = Context()
resolved = PermissionArticle.resolve_headline(MyType, Info())
assert resolved == 'headline'
def test_resolver_without_permission():
MyType = object()
class Viewer(object):
def has_perm(self, perm):
return False
class Info(object):
class Context(object):
user = Viewer()
context = Context()
resolved = PermissionArticle.resolve_headline(MyType, Info())
assert resolved is None
def test_permission_resolver_to_field():
MyType = object()
class Viewer(object):
def has_perm(self, perm):
return perm == 'content_type.permission3'
class Info(object):
class Context(object):
user = Viewer()
context = Context()
resolved = PermissionArticle.resolve_extra_field(MyType, Info())
assert resolved == 'extra field'
def test_resolver_to_field_without_permission():
MyType = object()
class Viewer(object):
def has_perm(self, perm):
return perm != 'content_type.permission3'
class Info(object):
class Context(object):
user = Viewer()
context = Context()
resolved = PermissionArticle.resolve_extra_field(MyType, Info())
assert resolved is None

View File

@ -1,14 +1,15 @@
from collections import OrderedDict
from functools import partial
from django.utils.functional import SimpleLazyObject
from graphene import Field
from graphene import Field, NonNull
from graphene.relay import Connection, Node
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
from graphene.types.utils import yank_fields_from_attrs
from .converter import convert_django_field_with_choices
from .registry import Registry, get_global_registry
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model, auth_resolver
def construct_fields(model, registry, only_fields, exclude_fields):
@ -32,6 +33,17 @@ def construct_fields(model, registry, only_fields, exclude_fields):
return fields
def get_auth_resolver(name, permissions, resolver=None):
"""
Get middleware resolver to handle field permissions
:param name: Field name
:param permissions: List of permissions
:param resolver: Field resolver
:return: Middleware resolver to check permissions
"""
return partial(auth_resolver, resolver, permissions, name, None, False)
class DjangoObjectTypeOptions(ObjectTypeOptions):
model = None # type: Model
registry = None # type: Registry
@ -41,6 +53,22 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
class DjangoObjectType(ObjectType):
"""
DjangoObjectType inheritance to handle field authorization
Accepts field's permissions description as:
class Meta:
field_to_permission = {
'restricted_field': ('permission1', 'permission2')
}
permission_to_field = {
'permission': ('restricted_field_1', 'restricted_field_2')
}
At least one of the permissions must be accomplished in order to resolve the field.
"""
@classmethod
def __init_subclass_with_meta__(
cls,
@ -54,6 +82,8 @@ class DjangoObjectType(ObjectType):
connection_class=None,
use_connection=None,
interfaces=(),
field_to_permission=None,
permission_to_field=None,
_meta=None,
**options
):
@ -104,13 +134,78 @@ class DjangoObjectType(ObjectType):
_meta.fields = django_fields
_meta.connection = connection
field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field)
if field_permissions:
cls.__set_as_nullable__(field_permissions, model, registry)
super(DjangoObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, interfaces=interfaces, **options
)
if field_permissions:
cls.__set_permissions_resolvers__(field_permissions)
cls.field_permissions = field_permissions
if not skip_registry:
registry.register(cls)
@classmethod
def __get_field_permissions__(cls, field_to_permission, permission_to_field):
"""Combines permissions from meta"""
permissions = field_to_permission if field_to_permission else {}
if permission_to_field:
perm_to_field = cls.__get_permission_to_fields__(permission_to_field)
for field, perms in perm_to_field.items():
if field in permissions:
permissions[field] += perms
else:
permissions[field] = perms
return permissions
@classmethod
def __get_permission_to_fields__(cls, permission_to_field):
"""
Accepts a dictionary like
{
permission: [fields]
}
:return: Mapping of fields to permissions like
{
field: [permissions]
}
"""
permissions = {}
for permission, fields in permission_to_field.items():
for field in fields:
permissions[field] = permissions.get(field, ()) + (permission,)
return permissions
@classmethod
def __set_permissions_resolvers__(cls, permissions):
"""Set permission resolvers"""
for field_name, field_permissions in permissions.items():
attr = 'resolve_{}'.format(field_name)
resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None)
if not hasattr(field_permissions, '__iter__'):
field_permissions = tuple(field_permissions)
setattr(cls, attr, get_auth_resolver(field_name, field_permissions, resolver))
@classmethod
def __set_as_nullable__(cls, field_permissions, model, registry):
"""Set restricted fields as nullable"""
django_fields = yank_fields_from_attrs(
construct_fields(model, registry, field_permissions.keys(), ()),
_as=Field,
)
for name, field in django_fields.items():
if hasattr(field, '_type') and isinstance(field._type, NonNull):
field._type = field._type._of_type
setattr(cls, name, field)
def resolve_id(self, info):
return self.pk

View File

@ -6,6 +6,7 @@ from django.db.models.manager import Manager
# from graphene.utils import LazyList
from graphene.types.resolver import get_default_resolver
from graphene.utils.get_unbound_function import get_unbound_function
@ -110,11 +111,13 @@ def resolve_bound_resolver(resolver, root, info, **args):
return resolver(root, info, **args)
def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **args):
def auth_resolver(parent_resolver, permissions, attname, default_value, raise_exception, root, info, **args):
"""
Middleware resolver to check viewer's permissions
:param parent_resolver: Field resolver
:param permissions: Field permissions
:param attname: Field name
:param default_value: Default value to field if no resolver is provided
:param raise_exception: If True a PermissionDenied is raised
:param root: Schema root
:param info: Schema info
@ -130,6 +133,8 @@ def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **a
if parent_resolver:
# A resolver is provided in the class
return resolve_bound_resolver(parent_resolver, root, info, **args)
# Get default resolver
return get_default_resolver()(attname, default_value, root, info, **args)
elif raise_exception:
raise PermissionDenied()
return None