Skip to content

Commit

Permalink
Dynamic trees: add support for user-defined tree item access checks (c…
Browse files Browse the repository at this point in the history
…loses #314)
  • Loading branch information
idlesign committed Dec 24, 2023
1 parent 11aab66 commit 38c48e5
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 16 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ django-sitetree changelog

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


Expand Down
22 changes: 22 additions & 0 deletions docs/source/apps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,25 @@ in ``urls.py`` of your project.
.. note:: If you use only dynamic trees you can set ``SITETREE_DYNAMIC_ONLY = True`` to prevent the application
from querying trees and items stored in DB.


Access check for dynamic items
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For dynamic trees you can implement access on per tree item basis.

Pass an access checking function in ``access_check`` argument.

.. note:: This function must accept ``tree`` argument and support pickling (e.g. be exposed on a module level).


.. code-block:: python
def check_user_is_staff(tree):
return tree.current_request.user.is_staff
...
item('dynamic_2', 'dynamic_2_url', access_check=check_user_is_staff),
...
62 changes: 54 additions & 8 deletions sitetree/sitetreeapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,40 +887,86 @@ def apply_hook(self, items: List['TreeItemBase'], sender: str) -> List['TreeItem

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

def check_access(self, item: 'TreeItemBase', context: Context) -> bool:
"""Checks whether a current user has an access to a certain item.
def check_access_auth(self, item: 'TreeItemBase', context: Context) -> bool:
"""Performs authentication related checks: whether the current user has an access to a certain item.
:param item:
:param context:
"""
if hasattr(self.current_request.user.is_authenticated, '__call__'):
authenticated = self.current_request.user.is_authenticated()
else:
authenticated = self.current_request.user.is_authenticated
authenticated = self.current_request.user.is_authenticated

if hasattr(authenticated, '__call__'):
authenticated = authenticated()

if item.access_loggedin and not authenticated:
return False

if item.access_guest and authenticated:
return False

return True

def check_access_perms(self, item: 'TreeItemBase', context: Context) -> bool:
"""Performs permissions related checks: whether the current user has an access to a certain item.
:param item:
:param context:
"""
if item.access_restricted:
user_perms = self._current_user_permissions

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

perms = item.perms # noqa dynamic attr

if item.access_perm_type == MODEL_TREE_ITEM_CLASS.PERM_TYPE_ALL:
if len(item.perms) != len(item.perms.intersection(user_perms)): # noqa dynamic attr
if len(perms) != len(perms.intersection(user_perms)):
return False
else:
if not len(item.perms.intersection(user_perms)): # noqa dynamic attr
if not len(perms.intersection(user_perms)):
return False

return True

def check_access_dyn(self, item: 'TreeItemBase', context: Context) -> Optional[bool]:
"""Performs dynamic item access check.
:param item: The item is expected to have `access_check` callable attribute implementing the check.
:param context:
"""
result = None
access_check_func = getattr(item, 'access_check', None)

if access_check_func:
return access_check_func(tree=self)

return None

def check_access(self, item: 'TreeItemBase', context: Context) -> bool:
"""Checks whether a current user has an access to a certain item.
:param item:
:param context:
"""
dyn_check = self.check_access_dyn(item, context)

if dyn_check is not None:
return dyn_check

if not self.check_access_auth(item, context):
return False

if not self.check_access_perms(item, context):
return False

return True

def get_permissions(self, user: 'User', item: 'TreeItemBase') -> set:
"""Returns a set of user and group level permissions for a given user.
Expand Down
17 changes: 16 additions & 1 deletion sitetree/tests/test_dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ def test_dynamic_only(template_render_tag, template_context, template_strip_tags
assert 'dynamic1_1' in result


dynamic_access_checked = []


def dynamic_access_check_it(tree):
dynamic_access_checked.append('yes')
return True


def test_dynamic_basic(template_render_tag, template_context, template_strip_tags):

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

item_dyn_access_check = item(
'dynamic1_1', '/dynamic1_1_url', url_as_pattern=False, sort_order=2,
access_check=dynamic_access_check_it
)
assert item_dyn_access_check.access_check is dynamic_access_check_it

trees = [
compose_dynamic_tree([tree('dynamic1', items=[
item('dynamic1_1', '/dynamic1_1_url', url_as_pattern=False, sort_order=2),
item_dyn_access_check,
item('dynamic1_2', '/dynamic1_2_url', url_as_pattern=False, sort_order=1),
])]),
compose_dynamic_tree([tree('dynamic2', items=[
Expand All @@ -40,6 +54,7 @@ def test_dynamic_basic(template_render_tag, template_context, template_strip_tag

assert 'dynamic1_1|dynamic1_2' in result
assert 'dynamic2_1' not in result
assert dynamic_access_checked == ['yes']

register_dynamic_trees(trees)

Expand Down
24 changes: 18 additions & 6 deletions sitetree/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from importlib import import_module
from types import ModuleType
from typing import Any, Sequence, Type, Union, List, Optional, Tuple
from typing import Any, Sequence, Type, Union, List, Optional, Tuple, Callable

from django.apps import apps
from django.contrib.auth.models import Permission
from django.core.exceptions import ImproperlyConfigured
from django.template.context import Context
from django.utils.functional import SimpleLazyObject
from django.utils.module_loading import module_has_submodule

Expand Down Expand Up @@ -101,6 +102,7 @@ def item(
access_by_perms: Union[TypePermission, List[TypePermission]] = None,
perms_mode_all: bool = True,
dynamic_attrs: Optional[dict] = None,
access_check: Optional[Callable[[Context], Optional[bool]]] = None,
**kwargs
) -> 'TreeItemBase':
"""Dynamically creates and returns a sitetree item object.
Expand Down Expand Up @@ -141,7 +143,14 @@ def item(
True - user should have all the permissions;
False - user should have any of chosen permissions.
:param dynamic_attrs: dynamic attributes to be attached to the item runtime.
:param dynamic_attrs: dynamic attributes to be attached to the item runtime
:param access_check: a callable to perform a custom item access check
Requires to accept `tree` named parameter (current user is in `tree.current_request.user`).
Boolean return is considered as an access check result.
None return instructs sitetree to process with other common access checks.
.. note:: This callable must support pickling (e.g. be exposed on a module level).
"""
item_obj = get_tree_item_model()(
Expand All @@ -161,10 +170,13 @@ def item(
if access_by_perms:
item_obj.access_restricted = True

if children is not None:
for child in children:
child.parent = item_obj
item_obj.dynamic_children.append(child)
if access_check:
item_obj.access_check = access_check

children = children or []
for child in children:
child.parent = item_obj
item_obj.dynamic_children.append(child)

dynamic_attrs = dynamic_attrs or {}
for key, value in dynamic_attrs.items():
Expand Down

0 comments on commit 38c48e5

Please sign in to comment.