216 lines
8.4 KiB
Python
216 lines
8.4 KiB
Python
|
|
from django.contrib.auth import get_permission_codename, get_user_model
|
||
|
|
from django.db.models import Q
|
||
|
|
|
||
|
|
from wagtail.models import GroupPagePermission, Page
|
||
|
|
from wagtail.permission_policies.base import OwnershipPermissionPolicy
|
||
|
|
|
||
|
|
|
||
|
|
class PagePermissionPolicy(OwnershipPermissionPolicy):
|
||
|
|
permission_cache_name = "_page_permission_cache"
|
||
|
|
_explorable_root_instance_cache_name = "_explorable_root_page_cache"
|
||
|
|
|
||
|
|
def __init__(self, model=Page):
|
||
|
|
super().__init__(model=model)
|
||
|
|
|
||
|
|
def get_all_permissions_for_user(self, user):
|
||
|
|
if not user.is_active or user.is_anonymous or user.is_superuser:
|
||
|
|
return GroupPagePermission.objects.none()
|
||
|
|
return GroupPagePermission.objects.filter(group__user=user).select_related(
|
||
|
|
"page", "permission"
|
||
|
|
)
|
||
|
|
|
||
|
|
def _base_user_has_permission(self, user):
|
||
|
|
if not user.is_active:
|
||
|
|
return False
|
||
|
|
if user.is_superuser:
|
||
|
|
return True
|
||
|
|
return None
|
||
|
|
|
||
|
|
def _base_queryset_for_user(self, user):
|
||
|
|
if not user.is_active:
|
||
|
|
return self.model._default_manager.none()
|
||
|
|
if user.is_superuser:
|
||
|
|
return self.model._default_manager.all()
|
||
|
|
return None
|
||
|
|
|
||
|
|
def user_has_permission(self, user, action):
|
||
|
|
return self.user_has_any_permission(user, {action})
|
||
|
|
|
||
|
|
def user_has_any_permission(self, user, actions):
|
||
|
|
base_permission = self._base_user_has_permission(user)
|
||
|
|
if base_permission is not None:
|
||
|
|
return base_permission
|
||
|
|
|
||
|
|
# User with only "add" permission can still edit their own pages
|
||
|
|
actions = set(actions)
|
||
|
|
if "change" in actions:
|
||
|
|
actions.add("add")
|
||
|
|
|
||
|
|
permissions = {
|
||
|
|
perm.permission.codename
|
||
|
|
for perm in self.get_cached_permissions_for_user(user)
|
||
|
|
}
|
||
|
|
return bool(self._get_permission_codenames(actions) & permissions)
|
||
|
|
|
||
|
|
def users_with_any_permission(self, actions, include_superusers=True):
|
||
|
|
# User with only "add" permission can still edit their own pages
|
||
|
|
actions = set(actions)
|
||
|
|
if "change" in actions:
|
||
|
|
actions.add("add")
|
||
|
|
|
||
|
|
groups = GroupPagePermission.objects.filter(
|
||
|
|
permission__codename__in=self._get_permission_codenames(actions)
|
||
|
|
).values_list("group", flat=True)
|
||
|
|
|
||
|
|
q = Q(groups__in=groups)
|
||
|
|
if include_superusers:
|
||
|
|
q |= Q(is_superuser=True)
|
||
|
|
|
||
|
|
return (
|
||
|
|
get_user_model()
|
||
|
|
._default_manager.filter(is_active=True)
|
||
|
|
.filter(q)
|
||
|
|
.distinct()
|
||
|
|
)
|
||
|
|
|
||
|
|
def users_with_permission(self, action, include_superusers=True):
|
||
|
|
return self.users_with_any_permission({action}, include_superusers)
|
||
|
|
|
||
|
|
def user_has_permission_for_instance(self, user, action, instance):
|
||
|
|
return self.user_has_any_permission_for_instance(user, {action}, instance)
|
||
|
|
|
||
|
|
def user_has_any_permission_for_instance(self, user, actions, instance):
|
||
|
|
base_permission = self._base_user_has_permission(user)
|
||
|
|
if base_permission is not None:
|
||
|
|
return base_permission
|
||
|
|
|
||
|
|
permissions = set()
|
||
|
|
for perm in self.get_cached_permissions_for_user(user):
|
||
|
|
if instance.pk == perm.page_id or instance.is_descendant_of(perm.page):
|
||
|
|
permissions.add(perm.permission.codename)
|
||
|
|
if (
|
||
|
|
perm.permission.codename
|
||
|
|
== get_permission_codename("add", self.model._meta)
|
||
|
|
and instance.owner_id == user.pk
|
||
|
|
):
|
||
|
|
permissions.add(get_permission_codename("change", self.model._meta))
|
||
|
|
|
||
|
|
return bool(self._get_permission_codenames(actions) & permissions)
|
||
|
|
|
||
|
|
def instances_user_has_any_permission_for(self, user, actions):
|
||
|
|
base_queryset = self._base_queryset_for_user(user)
|
||
|
|
if base_queryset is not None:
|
||
|
|
return base_queryset
|
||
|
|
|
||
|
|
pages = self.model._default_manager.none()
|
||
|
|
for perm in self.get_cached_permissions_for_user(user):
|
||
|
|
if (
|
||
|
|
perm.permission.codename
|
||
|
|
== get_permission_codename("add", self.model._meta)
|
||
|
|
and "add" not in actions
|
||
|
|
and "change" in actions
|
||
|
|
):
|
||
|
|
pages |= self.model._default_manager.descendant_of(
|
||
|
|
perm.page, inclusive=True
|
||
|
|
).filter(owner=user)
|
||
|
|
elif perm.permission.codename in self._get_permission_codenames(actions):
|
||
|
|
pages |= self.model._default_manager.descendant_of(
|
||
|
|
perm.page, inclusive=True
|
||
|
|
)
|
||
|
|
return pages
|
||
|
|
|
||
|
|
def users_with_any_permission_for_instance(
|
||
|
|
self, actions, instance, include_superusers=True
|
||
|
|
):
|
||
|
|
# Find permissions for all ancestors that match any of the actions
|
||
|
|
ancestors = instance.get_ancestors(inclusive=True)
|
||
|
|
groups = GroupPagePermission.objects.filter(
|
||
|
|
permission__codename__in=self._get_permission_codenames(actions),
|
||
|
|
page__in=ancestors,
|
||
|
|
).values_list("group", flat=True)
|
||
|
|
|
||
|
|
q = Q(groups__in=groups)
|
||
|
|
|
||
|
|
if include_superusers:
|
||
|
|
q |= Q(is_superuser=True)
|
||
|
|
|
||
|
|
# If "change" is in actions but "add" is not, then we need to check for
|
||
|
|
# cases where the user has "add" permission on an ancestor, and is the
|
||
|
|
# owner of the instance
|
||
|
|
if "change" in actions and "add" not in actions:
|
||
|
|
add_groups = GroupPagePermission.objects.filter(
|
||
|
|
permission__codename=get_permission_codename("add", self.model._meta),
|
||
|
|
page__in=ancestors,
|
||
|
|
).values_list("group", flat=True)
|
||
|
|
|
||
|
|
q |= Q(groups__in=add_groups) & Q(pk=instance.owner_id)
|
||
|
|
|
||
|
|
return (
|
||
|
|
get_user_model()
|
||
|
|
._default_manager.filter(is_active=True)
|
||
|
|
.filter(q)
|
||
|
|
.distinct()
|
||
|
|
)
|
||
|
|
|
||
|
|
def users_with_permission_for_instance(
|
||
|
|
self, action, instance, include_superusers=True
|
||
|
|
):
|
||
|
|
return self.users_with_any_permission_for_instance(
|
||
|
|
{action}, instance, include_superusers
|
||
|
|
)
|
||
|
|
|
||
|
|
def instances_with_direct_explore_permission(self, user):
|
||
|
|
# Get all pages that the user has direct add/change/publish/lock permission on
|
||
|
|
if user.is_superuser:
|
||
|
|
# superuser has implicit permission on the root node
|
||
|
|
return Page.objects.filter(depth=1)
|
||
|
|
else:
|
||
|
|
codenames = self._get_permission_codenames(
|
||
|
|
{"add", "change", "publish", "lock", "unlock", "bulk_delete"}
|
||
|
|
)
|
||
|
|
return [
|
||
|
|
perm.page
|
||
|
|
for perm in self.get_cached_permissions_for_user(user)
|
||
|
|
if perm.permission.codename in codenames
|
||
|
|
]
|
||
|
|
|
||
|
|
def explorable_root_instance(self, user):
|
||
|
|
# This method is used all around the admin,
|
||
|
|
# so cache the result on the user for the duration of the request
|
||
|
|
if hasattr(user, self._explorable_root_instance_cache_name):
|
||
|
|
return getattr(user, self._explorable_root_instance_cache_name)
|
||
|
|
pages = self.instances_with_direct_explore_permission(user)
|
||
|
|
try:
|
||
|
|
root_page = Page.objects.first_common_ancestor_of(
|
||
|
|
pages, include_self=True, strict=True
|
||
|
|
)
|
||
|
|
except Page.DoesNotExist:
|
||
|
|
root_page = None
|
||
|
|
setattr(user, self._explorable_root_instance_cache_name, root_page)
|
||
|
|
return root_page
|
||
|
|
|
||
|
|
def explorable_instances(self, user):
|
||
|
|
base_queryset = self._base_queryset_for_user(user)
|
||
|
|
if base_queryset is not None:
|
||
|
|
return base_queryset
|
||
|
|
|
||
|
|
explorable_pages = self.instances_user_has_any_permission_for(
|
||
|
|
user, {"add", "change", "publish", "lock", "unlock", "bulk_delete"}
|
||
|
|
)
|
||
|
|
|
||
|
|
# For all pages with specific permissions, add their ancestors as
|
||
|
|
# explorable. This will allow deeply nested pages to be accessed in the
|
||
|
|
# explorer. For example, in the hierarchy A>B>C>D where the user has
|
||
|
|
# 'change' access on D, they will be able to navigate to D without having
|
||
|
|
# explicit access to A, B or C.
|
||
|
|
page_permissions = [
|
||
|
|
perm.page for perm in self.get_cached_permissions_for_user(user)
|
||
|
|
]
|
||
|
|
for page in page_permissions:
|
||
|
|
explorable_pages |= page.get_ancestors()
|
||
|
|
|
||
|
|
# Remove unnecessary top-level ancestors that the user has no access to
|
||
|
|
fca_page = Page.objects.first_common_ancestor_of(page_permissions)
|
||
|
|
explorable_pages = explorable_pages.filter(path__startswith=fca_page.path)
|
||
|
|
return explorable_pages
|