Agregar permisos por Type (#29)

* Adding permission_to_all_fields to DjangoObjectType meta data

* Adding permission_to_all_fields to DjangoObjectType meta data

* Adding permission_to_all_fields to DjangoObjectType meta data

* change raise_exception

* Adding custom PermissionDenied

* Adding custom PermissionDenied

* Adding custom PermissionDenied

* adding prints

* adding prints

* Removing prints

* Adding prints

* Adding prints

* Change permissions

* Fixing pylint

* Change __get_field_permissions__ method

* Revert __set_as_nullable__ method

* Change permissions

* Fixing resolvers

* Merge django fields with meta fields

* In get_resolver take parent_resolver when it have added permissions

* In get_resolver take parent_resolver when it have added permissions

* Change user_logged param for user

* Merging

* Fixing tests

* adding prints

* adding prints

* revert

* revert

* Testing

* Testing

* Fixing tests

* Fixing tests

* Testing

* Fixing bugs

* Fixing pylint

* Adding resolve_fields from interfaces

* Adding resolve_fields from interfaces

Co-authored-by: ariel1899 <radke1899@gmail.com>
This commit is contained in:
alejandronunez 2020-12-11 07:56:10 -07:00 committed by GitHub
parent ae663474e0
commit 133a597f6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 83 deletions

View File

@ -219,7 +219,10 @@ class DjangoField(Field):
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
"""Intercept resolver to analyse permissions""" """Intercept resolver to analyse permissions"""
parent_resolver = super(DjangoField, self).get_resolver(parent_resolver)
if getattr(parent_resolver, "func", None) != auth_resolver:
parent_resolver = super(DjangoField, self).get_resolver(parent_resolver)
if self.permissions: if self.permissions:
return partial( return partial(
get_unbound_function(self.permissions_resolver), get_unbound_function(self.permissions_resolver),

View File

@ -5,7 +5,7 @@ import pytest
from graphene import List, NonNull, ObjectType, Schema, String from graphene import List, NonNull, ObjectType, Schema, String
from mock import mock from mock import mock
from unittest import TestCase from unittest import TestCase
from django.core.exceptions import PermissionDenied from graphene_django.utils.utils import PermissionDenied
from graphene_django.fields import DjangoField, DataLoaderField from graphene_django.fields import DjangoField, DataLoaderField
from promise.dataloader import DataLoader from promise.dataloader import DataLoader
from promise import Promise from promise import Promise

View File

@ -3,11 +3,13 @@ from textwrap import dedent
import pytest import pytest
from django.db import models from django.db import models
from django.test import TestCase
from mock import patch from mock import patch
from graphene import Interface, ObjectType, Schema, Connection, String, Field from graphene import Interface, ObjectType, Schema, Connection, String, Field
from graphene.relay import Node from graphene.relay import Node
from graphene_django.utils.utils import PermissionDenied
from .. import registry from .. import registry
from ..settings import graphene_settings from ..settings import graphene_settings
from ..types import DjangoObjectType, DjangoObjectTypeOptions from ..types import DjangoObjectType, DjangoObjectTypeOptions
@ -581,7 +583,9 @@ def extra_field_resolver(root, info, **kwargs):
class PermissionArticle(DjangoObjectType): class PermissionArticle(DjangoObjectType):
"""Basic Type to test""" """Basic Type to test"""
class Meta(object): extra_field = Field(String, resolver=extra_field_resolver)
class Meta:
"""Meta Class""" """Meta Class"""
field_to_permission = { field_to_permission = {
@ -593,85 +597,81 @@ class PermissionArticle(DjangoObjectType):
} }
model = ArticleModel model = ArticleModel
extra_field = Field(String, resolver=extra_field_resolver)
def resolve_headline(self, info, **kwargs): def resolve_headline(self, info, **kwargs):
return "headline" return "headline"
def test_django_permissions(): class PermissionTypesTests(TestCase):
expected = { def test_django_permissions(self):
"headline": ("content_type.permission1", "content_type.permission3"), expected = {
"pub_date": ("content_type.permission2",), "headline": ("content_type.permission1", "content_type.permission3"),
"reporter": ("content_type.permission3",), "pub_date": ("content_type.permission2",),
"extra_field": ("content_type.permission3",), "reporter": ("content_type.permission3",),
} "extra_field": ("content_type.permission3",),
assert PermissionArticle.field_permissions == expected }
self.assertEqual(PermissionArticle.field_permissions, expected)
def test_permission_resolver(): def test_permission_resolver(self):
MyType = object() MyType = object()
class Viewer(object): class Viewer(object):
def has_perm(self, perm): def has_perm(self, perm):
return perm == "content_type.permission3" return perm == "content_type.permission3"
class Info(object): class Info(object):
class Context(object): class Context(object):
user = Viewer() user = Viewer()
context = Context() context = Context()
resolved = PermissionArticle.resolve_headline(MyType, Info()) resolved = PermissionArticle.resolve_headline(MyType, Info())
assert resolved == "headline" self.assertEqual(resolved, "headline")
def test_resolver_without_permission(self):
MyType = object()
def test_resolver_without_permission(): class Viewer(object):
MyType = object() def has_perm(self, perm):
return False
class Viewer(object): class Info(object):
def has_perm(self, perm): class Context(object):
return False user = Viewer()
class Info(object): context = Context()
class Context(object):
user = Viewer()
context = Context() with self.assertRaises(PermissionDenied):
PermissionArticle.resolve_headline(MyType, Info())
resolved = PermissionArticle.resolve_headline(MyType, Info()) def test_permission_resolver_to_field(self):
assert resolved is None MyType = object()
class Viewer(object):
def has_perm(self, perm):
return perm == "content_type.permission3"
def test_permission_resolver_to_field(): class Info(object):
MyType = object() class Context(object):
user = Viewer()
class Viewer(object): context = Context()
def has_perm(self, perm):
return perm == "content_type.permission3"
class Info(object): resolved = PermissionArticle.resolve_extra_field(MyType, Info())
class Context(object): self.assertEqual(resolved, "extra field")
user = Viewer()
context = Context() def test_resolver_to_field_without_permission(self):
MyType = object()
resolved = PermissionArticle.resolve_extra_field(MyType, Info()) class Viewer(object):
assert resolved == "extra field" def has_perm(self, perm):
return perm != "content_type.permission3"
class Info(object):
class Context(object):
user = Viewer()
def test_resolver_to_field_without_permission(): context = Context()
MyType = object()
class Viewer(object): resolved = PermissionArticle.resolve_extra_field(MyType, Info())
def has_perm(self, perm): self.assertIsNone(resolved)
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

@ -19,7 +19,6 @@ from .registry import Registry, get_global_registry
from .settings import graphene_settings from .settings import graphene_settings
from .utils import ( from .utils import (
DJANGO_FILTER_INSTALLED, DJANGO_FILTER_INSTALLED,
camelize,
get_model_fields, get_model_fields,
is_valid_django_model, is_valid_django_model,
) )
@ -27,7 +26,6 @@ from .utils import (
if six.PY3: if six.PY3:
from typing import Type from typing import Type
ALL_FIELDS = "__all__" ALL_FIELDS = "__all__"
@ -133,15 +131,27 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields):
) )
def get_auth_resolver(name, permissions, resolver=None): def get_auth_resolver(
name, permissions, resolver=None, raise_exception=False, permission_classes=None
):
""" """
Get middleware resolver to handle field permissions Get middleware resolver to handle field permissions
:param name: Field name :param name: Field name
:param permissions: List of permissions :param permissions: List of permissions
:param resolver: Field resolver :param resolver: Field resolver
:param raise_exception: If True a PermissionDenied is raised
:param permission_classes: Permission for user
:return: Middleware resolver to check permissions :return: Middleware resolver to check permissions
""" """
return partial(auth_resolver, resolver, permissions, name, None, False) return partial(
auth_resolver,
resolver,
permissions,
name,
None,
raise_exception,
permission_classes=permission_classes,
)
class DjangoObjectTypeOptions(ObjectTypeOptions): class DjangoObjectTypeOptions(ObjectTypeOptions):
@ -173,6 +183,7 @@ class DjangoObjectType(ObjectType):
convert_choices_to_enum=True, convert_choices_to_enum=True,
field_to_permission=None, field_to_permission=None,
permission_to_field=None, permission_to_field=None,
permission_to_all_fields=None,
_meta=None, _meta=None,
**options **options
): ):
@ -273,21 +284,26 @@ class DjangoObjectType(ObjectType):
_meta.fields = django_fields _meta.fields = django_fields
_meta.connection = connection _meta.connection = connection
field_permissions = cls.__get_field_permissions__( permission_classes = getattr(cls, "permission_classes", None)
field_to_permission, permission_to_field
)
if field_permissions:
cls.__set_as_nullable__(field_permissions, model, registry)
super(DjangoObjectType, cls).__init_subclass_with_meta__( super(DjangoObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, interfaces=interfaces, **options _meta=_meta, interfaces=interfaces, **options
) )
field_permissions, fields_raise_exception = cls.__get_field_permissions__(
field_to_permission,
permission_to_field,
permission_to_all_fields,
permission_classes,
)
# Validate fields # Validate fields
validate_fields(cls, model, _meta.fields, fields, exclude) validate_fields(cls, model, _meta.fields, fields, exclude)
if field_permissions: if field_permissions:
cls.__set_permissions_resolvers__(field_permissions) cls.__set_permissions_resolvers__(
field_permissions, fields_raise_exception, permission_classes
)
cls.field_permissions = field_permissions cls.field_permissions = field_permissions
@ -295,18 +311,44 @@ class DjangoObjectType(ObjectType):
registry.register(cls) registry.register(cls)
@classmethod @classmethod
def __get_field_permissions__(cls, field_to_permission, permission_to_field): def __get_field_permissions__(
cls,
field_to_permission,
permission_to_field,
permission_to_all_fields,
permission_classes,
):
"""Combines permissions from meta""" """Combines permissions from meta"""
permissions = field_to_permission if field_to_permission else {} permissions = field_to_permission if field_to_permission else {}
if permission_to_field: perm_to_field = cls.__get_permission_to_fields__(
perm_to_field = cls.__get_permission_to_fields__(permission_to_field) permission_to_field if permission_to_field else {}
for field, perms in perm_to_field.items(): )
if field in permissions: fields_raise_exception = {}
permissions[field] += perms
else:
permissions[field] = perms
return permissions for name, field in cls._meta.fields.items():
if name == "id":
continue
if permission_classes:
permissions[name] = ()
if name in perm_to_field:
if name in permissions:
permissions[name] += perm_to_field[name]
else:
permissions[name] = perm_to_field[name]
if permission_to_all_fields:
permissions[name] = tuple(
set(permissions.get(name, ()) + permission_to_all_fields)
)
if name in permissions:
fields_raise_exception[name] = hasattr(field, "_type") and isinstance(
field._type, NonNull
)
return permissions, fields_raise_exception
@classmethod @classmethod
def __get_permission_to_fields__(cls, permission_to_field): def __get_permission_to_fields__(cls, permission_to_field):
@ -327,19 +369,41 @@ class DjangoObjectType(ObjectType):
return permissions return permissions
@classmethod @classmethod
def __set_permissions_resolvers__(cls, permissions): def __set_permissions_resolvers__(
cls, permissions, fields_raise_exception, permission_classes
):
"""Set permission resolvers""" """Set permission resolvers"""
for field_name, field_permissions in permissions.items(): for field_name, field_permissions in permissions.items():
raise_exception = fields_raise_exception.get(field_name, False)
attr = "resolve_{}".format(field_name) attr = "resolve_{}".format(field_name)
resolver = getattr( resolver = getattr(
cls._meta.fields[field_name], "resolver", None cls._meta.fields[field_name], "resolver", None
) or getattr(cls, attr, None) ) or getattr(cls, attr, None)
if not resolver:
for interface in cls._meta.interfaces:
resolver = getattr(
interface._meta.fields.get(field_name, None), "resolver", None
) or getattr(interface, attr, None)
if resolver:
break
if not hasattr(field_permissions, "__iter__"): if not hasattr(field_permissions, "__iter__"):
field_permissions = tuple(field_permissions) field_permissions = tuple(field_permissions)
setattr( setattr(
cls, attr, get_auth_resolver(field_name, field_permissions, resolver) cls,
attr,
get_auth_resolver(
field_name,
field_permissions,
resolver,
raise_exception,
permission_classes,
),
) )
@classmethod @classmethod

View File

@ -1,9 +1,10 @@
import inspect import inspect
import six import six
from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.db.models.manager import Manager from django.db.models.manager import Manager
from django.utils.translation import ugettext_lazy as _
from graphql.error import GraphQLError
# from graphene.utils import LazyList # from graphene.utils import LazyList
from graphene.types.resolver import get_default_resolver from graphene.types.resolver import get_default_resolver
@ -43,6 +44,20 @@ def camelize(data):
return data return data
class PermissionDenied(GraphQLError):
"""Exception for permission denied. This exception must be used when a user does not have access to a resource"""
message = _("Permission denied.")
code = "permission-denied"
def __init__(
self, nodes=None, stack=None, source=None, positions=None, locations=None
):
super(PermissionDenied, self).__init__(
self.__class__.message, nodes, stack, source, positions, locations
)
def get_reverse_fields(model, local_field_names): def get_reverse_fields(model, local_field_names):
for name, attr in model.__dict__.items(): for name, attr in model.__dict__.items():
# Don't duplicate any local fields # Don't duplicate any local fields
@ -159,7 +174,17 @@ def auth_resolver(
raise PermissionDenied() raise PermissionDenied()
user = info.context.user user = info.context.user
if has_permissions(user, permissions): permission_classes = args.pop("permission_classes", None)
if has_permissions(user, permissions) and (
not permission_classes
or all(
(
perm.has_permission(user=user, instance=root, **args)
for perm in permission_classes
)
)
):
if parent_resolver: if parent_resolver:
# A resolver is provided in the class # A resolver is provided in the class
return resolve_bound_resolver(parent_resolver, root, info, **args) return resolve_bound_resolver(parent_resolver, root, info, **args)