Skip to content

Commit 6923442

Browse files
committed
refactor permissions
1 parent be4051f commit 6923442

File tree

3 files changed

+46
-43
lines changed

3 files changed

+46
-43
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 & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
11
from inspect import isclass
2-
from simple_api.utils import ensure_tuple
32

43

5-
# instantiates permission classes (if they are not already, maybe due to get_fn injection) and builds a
6-
# function that raises if the permissions are not passed
74
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-
155
def fn(**kwargs):
16-
for perm in instantiated_permissions:
17-
if not perm.has_permission(**kwargs):
18-
raise PermissionError(perm.error_message(**kwargs))
6+
for perm in permissions:
7+
if not perm().has_permission(**kwargs):
8+
raise PermissionError(perm().error_message(**kwargs))
199
return fn
2010

2111

@@ -26,12 +16,21 @@ def __init__(self, **kwargs):
2616
def permission_statement(self, **kwargs):
2717
raise NotImplementedError
2818

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

3736
def error_message(self, **kwargs):
@@ -43,12 +42,10 @@ def __init__(self, *permissions):
4342
self.permissions = permissions
4443

4544
def __call__(self, **kwargs):
46-
instantiated_perms = []
4745
for perm in self.permissions:
4846
assert isclass(perm) or isinstance(perm, LogicalConnector), \
4947
"Permissions in logical connectors must be classes."
50-
instantiated_perms.append(perm(**kwargs))
51-
return LogicalResolver(instantiated_perms, self.resolve_fn)
48+
return LogicalResolver(self.permissions, self.resolve_fn)
5249

5350
def resolve_fn(self, permissions, **kwargs):
5451
raise NotImplementedError
@@ -69,23 +66,23 @@ def error_message(self, **kwargs):
6966
class Or(LogicalConnector):
7067
def resolve_fn(self, permissions, **kwargs):
7168
for perm in permissions:
72-
if perm.has_permission(**kwargs):
69+
if perm().has_permission(**kwargs):
7370
return True
7471
return False
7572

7673

7774
class And(LogicalConnector):
7875
def resolve_fn(self, permissions, **kwargs):
7976
for perm in permissions:
80-
if not perm.has_permission(**kwargs):
77+
if not perm().has_permission(**kwargs):
8178
return False
8279
return True
8380

8481

8582
class Not(LogicalConnector):
8683
def resolve_fn(self, permissions, **kwargs):
8784
assert len(permissions) == 1, "`Not` accepts only one permission class as parameter."
88-
return not permissions[0].has_permission(**kwargs)
85+
return not permissions[0]().has_permission(**kwargs)
8986

9087

9188
class AllowAll(BasePermission):

0 commit comments

Comments
 (0)