Skip to content

Commit 9e46b75

Browse files
committed
refactor permissions
1 parent be4051f commit 9e46b75

File tree

3 files changed

+46
-40
lines changed

3 files changed

+46
-40
lines changed

simple_api/django_object/actions.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from simple_api.django_object.utils import determine_items, remove_item
44
from simple_api.object.actions import Action, ToActionMixin, SetReferencesMixin
55
from simple_api.object.datatypes import ObjectType, BooleanType
6+
from .permissions import permission_class_with_get_fn
7+
from ..utils import ensure_tuple
68

79

810
class WithObjectMixin:
@@ -23,7 +25,7 @@ def __init__(self, parameters=None, data=None, return_value=None, exec_fn=None,
2325
self.data = data or {}
2426
self.return_value = return_value or ObjectType("self")
2527
self.exec_fn = exec_fn
26-
self.permissions = permissions
28+
self.permissions = ensure_tuple(permissions)
2729
self.kwargs = kwargs
2830

2931
# these attributes exist to ensure that 1) the input from the user is stored unmodified in the attributes above
@@ -111,17 +113,15 @@ def fn(request, params, obj, **kwargs):
111113
# injects get_fn into the permission classes so that this information can be used to determine permissions
112114
def determine_permissions(self):
113115
if self._determined_permissions is None:
114-
self._determined_permissions = self.permissions
115-
if self._determined_permissions is None:
116-
return
117-
if not isinstance(self._determined_permissions, (list, tuple)):
118-
self._determined_permissions = self._determined_permissions,
119-
116+
self._determined_permissions = []
120117
self.determine_get_fn()
121-
instantiated_permissions = []
122-
for pc in self._determined_permissions:
123-
instantiated_permissions.append(pc(get_fn=self._determined_get_fn))
124-
self._determined_permissions = instantiated_permissions
118+
for pc in self.permissions:
119+
# here we extract the information from a permission class and use it to build a new class with the
120+
# function to get the related object embedded; this is because permissions are supposed to be classes
121+
# and not their instances and since django actions are implemented as composition with respect to
122+
# actions, there would be a problem with actions seeing instantiated permission classes, which would
123+
# create problems later
124+
self._determined_permissions.append(permission_class_with_get_fn(pc, self._determined_get_fn))
125125

126126
def determine_parameters(self):
127127
if self._determined_parameters is None:

simple_api/django_object/permissions.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
from simple_api.object.permissions import BasePermission
22

33

4-
class DjangoPermission(BasePermission):
5-
def __init__(self, get_fn=None, **kwargs):
6-
self.get_fn = get_fn
7-
super().__init__(**kwargs)
4+
def permission_class_with_get_fn(permission_class, get_fn):
5+
# takes a permission class and embeds the get_fn of the object into it; this is so that it can be passed around
6+
# as a class and there is no need to instantiate it (otherwise we would run into trying to instantiate something
7+
# that has already been instantiated)
8+
class _DjangoPermission:
9+
def has_permission(self, **kwargs):
10+
obj = kwargs.pop("obj")
11+
if obj is None:
12+
obj = get_fn(**kwargs)
13+
return permission_class().has_permission(obj=obj, **kwargs)
14+
15+
def error_message(self, **kwargs):
16+
return permission_class().error_message(**kwargs)
17+
return _DjangoPermission
818

9-
def has_permission(self, exclude_classes=(), **kwargs):
10-
obj = kwargs.pop("obj")
11-
if obj is None and self.get_fn is not None:
12-
obj = self.get_fn(**kwargs)
13-
return super().has_permission(exclude_classes=(DjangoPermission,) + exclude_classes, obj=obj, **kwargs)
1419

20+
class DjangoPermission(BasePermission):
1521
def permission_statement(self, request, obj, **kwargs):
1622
raise NotImplementedError
1723

simple_api/object/permissions.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,10 @@
55
# instantiates permission classes (if they are not already, maybe due to get_fn injection) and builds a
66
# function that raises if the permissions are not passed
77
def build_permissions_fn(permissions):
8-
instantiated_permissions = []
9-
for cls_or_inst in permissions:
10-
if isclass(cls_or_inst) or isinstance(cls_or_inst, LogicalConnector):
11-
instantiated_permissions.append(cls_or_inst())
12-
else:
13-
instantiated_permissions.append(cls_or_inst)
14-
158
def fn(**kwargs):
16-
for perm in instantiated_permissions:
17-
if not perm.has_permission(**kwargs):
18-
raise PermissionError(perm.error_message(**kwargs))
9+
for perm in permissions:
10+
if not perm().has_permission(**kwargs):
11+
raise PermissionError(perm().error_message(**kwargs))
1912
return fn
2013

2114

@@ -26,12 +19,21 @@ def __init__(self, **kwargs):
2619
def permission_statement(self, **kwargs):
2720
raise NotImplementedError
2821

29-
def has_permission(self, exclude_classes=(), **kwargs):
22+
def has_permission(self, **kwargs):
23+
# to achieve hierarchical checking for permissions (a subclass calls the permission statement of the superclass
24+
# and only if it passes, executes its own), we need to traverse over the whole linearization order of the
25+
# permission class; however, classes like object, which are naturally also a part of the chain, do not
26+
# contain the `permission_statement` method and therefore should be just skipped; the same is true for abstract
27+
# permission classes which contain the method, but is not implemented - like this one for example: to achieve
28+
# this, we try calling the method and if it turns out to not be implemented, we skip it as well
3029
for cls in reversed(self.__class__.__mro__):
31-
if cls in (object, BasePermission, LogicalResolver) + exclude_classes:
30+
if not hasattr(cls, "permission_statement"):
31+
continue
32+
try:
33+
if not cls.permission_statement(self, **kwargs):
34+
return False
35+
except NotImplementedError:
3236
continue
33-
if not cls.permission_statement(self, **kwargs):
34-
return False
3537
return True
3638

3739
def error_message(self, **kwargs):
@@ -43,12 +45,10 @@ def __init__(self, *permissions):
4345
self.permissions = permissions
4446

4547
def __call__(self, **kwargs):
46-
instantiated_perms = []
4748
for perm in self.permissions:
4849
assert isclass(perm) or isinstance(perm, LogicalConnector), \
4950
"Permissions in logical connectors must be classes."
50-
instantiated_perms.append(perm(**kwargs))
51-
return LogicalResolver(instantiated_perms, self.resolve_fn)
51+
return LogicalResolver(self.permissions, self.resolve_fn)
5252

5353
def resolve_fn(self, permissions, **kwargs):
5454
raise NotImplementedError
@@ -69,23 +69,23 @@ def error_message(self, **kwargs):
6969
class Or(LogicalConnector):
7070
def resolve_fn(self, permissions, **kwargs):
7171
for perm in permissions:
72-
if perm.has_permission(**kwargs):
72+
if perm().has_permission(**kwargs):
7373
return True
7474
return False
7575

7676

7777
class And(LogicalConnector):
7878
def resolve_fn(self, permissions, **kwargs):
7979
for perm in permissions:
80-
if not perm.has_permission(**kwargs):
80+
if not perm().has_permission(**kwargs):
8181
return False
8282
return True
8383

8484

8585
class Not(LogicalConnector):
8686
def resolve_fn(self, permissions, **kwargs):
8787
assert len(permissions) == 1, "`Not` accepts only one permission class as parameter."
88-
return not permissions[0].has_permission(**kwargs)
88+
return not permissions[0]().has_permission(**kwargs)
8989

9090

9191
class AllowAll(BasePermission):

0 commit comments

Comments
 (0)