mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-04-25 19:43:51 +03:00
Merge pull request #6 from revolico/permission-to-type#5
Permission to type#5
This commit is contained in:
commit
49bb86f58d
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user