Skip to content

Commit 38c48e5

Browse files
committed
Dynamic trees: add support for user-defined tree item access checks (closes #314)
1 parent 11aab66 commit 38c48e5

File tree

5 files changed

+112
-16
lines changed

5 files changed

+112
-16
lines changed

CHANGELOG

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ django-sitetree changelog
44

55
Unreleased
66
----------
7-
+ Dynamic trees: add 'dynamic_attrs' parameter support for item().
7+
+ Dynamic trees: add 'dynamic_attrs' parameter support for item() (closes #313).
8+
+ Dynamic trees: add support for user-defined tree item access checks (closes #314).
89
* Add QA for Python 3.11 and Django 5.0. Dropped QA for Python 3.6
910

1011

docs/source/apps.rst

+22
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,25 @@ in ``urls.py`` of your project.
104104
.. note:: If you use only dynamic trees you can set ``SITETREE_DYNAMIC_ONLY = True`` to prevent the application
105105
from querying trees and items stored in DB.
106106

107+
108+
Access check for dynamic items
109+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110+
111+
For dynamic trees you can implement access on per tree item basis.
112+
113+
Pass an access checking function in ``access_check`` argument.
114+
115+
.. note:: This function must accept ``tree`` argument and support pickling (e.g. be exposed on a module level).
116+
117+
118+
.. code-block:: python
119+
120+
def check_user_is_staff(tree):
121+
return tree.current_request.user.is_staff
122+
123+
...
124+
125+
item('dynamic_2', 'dynamic_2_url', access_check=check_user_is_staff),
126+
127+
...
128+

sitetree/sitetreeapp.py

+54-8
Original file line numberDiff line numberDiff line change
@@ -887,40 +887,86 @@ def apply_hook(self, items: List['TreeItemBase'], sender: str) -> List['TreeItem
887887

888888
return processor(tree_items=items, tree_sender=sender, context=self.current_page_context)
889889

890-
def check_access(self, item: 'TreeItemBase', context: Context) -> bool:
891-
"""Checks whether a current user has an access to a certain item.
890+
def check_access_auth(self, item: 'TreeItemBase', context: Context) -> bool:
891+
"""Performs authentication related checks: whether the current user has an access to a certain item.
892892
893893
:param item:
894894
:param context:
895895
896896
"""
897-
if hasattr(self.current_request.user.is_authenticated, '__call__'):
898-
authenticated = self.current_request.user.is_authenticated()
899-
else:
900-
authenticated = self.current_request.user.is_authenticated
897+
authenticated = self.current_request.user.is_authenticated
898+
899+
if hasattr(authenticated, '__call__'):
900+
authenticated = authenticated()
901901

902902
if item.access_loggedin and not authenticated:
903903
return False
904904

905905
if item.access_guest and authenticated:
906906
return False
907907

908+
return True
909+
910+
def check_access_perms(self, item: 'TreeItemBase', context: Context) -> bool:
911+
"""Performs permissions related checks: whether the current user has an access to a certain item.
912+
913+
:param item:
914+
:param context:
915+
916+
"""
908917
if item.access_restricted:
909918
user_perms = self._current_user_permissions
910919

911920
if user_perms is _UNSET:
912921
user_perms = self.get_permissions(self.current_request.user, item)
913922
self._current_user_permissions = user_perms
914923

924+
perms = item.perms # noqa dynamic attr
925+
915926
if item.access_perm_type == MODEL_TREE_ITEM_CLASS.PERM_TYPE_ALL:
916-
if len(item.perms) != len(item.perms.intersection(user_perms)): # noqa dynamic attr
927+
if len(perms) != len(perms.intersection(user_perms)):
917928
return False
918929
else:
919-
if not len(item.perms.intersection(user_perms)): # noqa dynamic attr
930+
if not len(perms.intersection(user_perms)):
920931
return False
921932

922933
return True
923934

935+
def check_access_dyn(self, item: 'TreeItemBase', context: Context) -> Optional[bool]:
936+
"""Performs dynamic item access check.
937+
938+
:param item: The item is expected to have `access_check` callable attribute implementing the check.
939+
:param context:
940+
941+
"""
942+
result = None
943+
access_check_func = getattr(item, 'access_check', None)
944+
945+
if access_check_func:
946+
return access_check_func(tree=self)
947+
948+
return None
949+
950+
def check_access(self, item: 'TreeItemBase', context: Context) -> bool:
951+
"""Checks whether a current user has an access to a certain item.
952+
953+
:param item:
954+
:param context:
955+
956+
"""
957+
dyn_check = self.check_access_dyn(item, context)
958+
959+
if dyn_check is not None:
960+
return dyn_check
961+
962+
if not self.check_access_auth(item, context):
963+
return False
964+
965+
if not self.check_access_perms(item, context):
966+
return False
967+
968+
return True
969+
924970
def get_permissions(self, user: 'User', item: 'TreeItemBase') -> set:
925971
"""Returns a set of user and group level permissions for a given user.
926972

sitetree/tests/test_dynamic.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ def test_dynamic_only(template_render_tag, template_context, template_strip_tags
1616
assert 'dynamic1_1' in result
1717

1818

19+
dynamic_access_checked = []
20+
21+
22+
def dynamic_access_check_it(tree):
23+
dynamic_access_checked.append('yes')
24+
return True
25+
26+
1927
def test_dynamic_basic(template_render_tag, template_context, template_strip_tags):
2028

2129
from sitetree.toolbox import compose_dynamic_tree, register_dynamic_trees, tree, item, get_dynamic_trees
@@ -24,9 +32,15 @@ def test_dynamic_basic(template_render_tag, template_context, template_strip_tag
2432
item_dyn_attrs = item('dynamic2_1', '/dynamic2_1_url', url_as_pattern=False, dynamic_attrs={'a': 'b'})
2533
assert item_dyn_attrs.a == 'b'
2634

35+
item_dyn_access_check = item(
36+
'dynamic1_1', '/dynamic1_1_url', url_as_pattern=False, sort_order=2,
37+
access_check=dynamic_access_check_it
38+
)
39+
assert item_dyn_access_check.access_check is dynamic_access_check_it
40+
2741
trees = [
2842
compose_dynamic_tree([tree('dynamic1', items=[
29-
item('dynamic1_1', '/dynamic1_1_url', url_as_pattern=False, sort_order=2),
43+
item_dyn_access_check,
3044
item('dynamic1_2', '/dynamic1_2_url', url_as_pattern=False, sort_order=1),
3145
])]),
3246
compose_dynamic_tree([tree('dynamic2', items=[
@@ -40,6 +54,7 @@ def test_dynamic_basic(template_render_tag, template_context, template_strip_tag
4054

4155
assert 'dynamic1_1|dynamic1_2' in result
4256
assert 'dynamic2_1' not in result
57+
assert dynamic_access_checked == ['yes']
4358

4459
register_dynamic_trees(trees)
4560

sitetree/utils.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from importlib import import_module
22
from types import ModuleType
3-
from typing import Any, Sequence, Type, Union, List, Optional, Tuple
3+
from typing import Any, Sequence, Type, Union, List, Optional, Tuple, Callable
44

55
from django.apps import apps
66
from django.contrib.auth.models import Permission
77
from django.core.exceptions import ImproperlyConfigured
8+
from django.template.context import Context
89
from django.utils.functional import SimpleLazyObject
910
from django.utils.module_loading import module_has_submodule
1011

@@ -101,6 +102,7 @@ def item(
101102
access_by_perms: Union[TypePermission, List[TypePermission]] = None,
102103
perms_mode_all: bool = True,
103104
dynamic_attrs: Optional[dict] = None,
105+
access_check: Optional[Callable[[Context], Optional[bool]]] = None,
104106
**kwargs
105107
) -> 'TreeItemBase':
106108
"""Dynamically creates and returns a sitetree item object.
@@ -141,7 +143,14 @@ def item(
141143
True - user should have all the permissions;
142144
False - user should have any of chosen permissions.
143145
144-
:param dynamic_attrs: dynamic attributes to be attached to the item runtime.
146+
:param dynamic_attrs: dynamic attributes to be attached to the item runtime
147+
148+
:param access_check: a callable to perform a custom item access check
149+
Requires to accept `tree` named parameter (current user is in `tree.current_request.user`).
150+
Boolean return is considered as an access check result.
151+
None return instructs sitetree to process with other common access checks.
152+
153+
.. note:: This callable must support pickling (e.g. be exposed on a module level).
145154
146155
"""
147156
item_obj = get_tree_item_model()(
@@ -161,10 +170,13 @@ def item(
161170
if access_by_perms:
162171
item_obj.access_restricted = True
163172

164-
if children is not None:
165-
for child in children:
166-
child.parent = item_obj
167-
item_obj.dynamic_children.append(child)
173+
if access_check:
174+
item_obj.access_check = access_check
175+
176+
children = children or []
177+
for child in children:
178+
child.parent = item_obj
179+
item_obj.dynamic_children.append(child)
168180

169181
dynamic_attrs = dynamic_attrs or {}
170182
for key, value in dynamic_attrs.items():

0 commit comments

Comments
 (0)