Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1 @@
from .base import * # NOQA: F403

View File

@@ -0,0 +1,427 @@
from django.contrib.auth import get_permission_codename, get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db.models import Q
from django.utils.functional import cached_property
from wagtail.coreutils import resolve_model_string
class BasePermissionPolicy:
"""
A 'permission policy' is an object that handles all decisions about the actions
users are allowed to perform on a given model. The mechanism by which it does this
is arbitrary, and may or may not involve the django.contrib.auth Permission model;
it could be as simple as "allow all users to do everything".
In this way, admin apps can change their permission-handling logic just by swapping
to a different policy object, rather than having that logic spread across numerous
view functions.
BasePermissionPolicy is an abstract class that all permission policies inherit from.
The only method that subclasses need to implement is users_with_any_permission;
all other methods can be derived from that (but in practice, subclasses will probably
want to override additional methods, either for efficiency or to implement more
fine-grained permission logic).
"""
permission_cache_name = ""
def __init__(self, model):
self._model_or_name = model
@cached_property
def model(self):
model = resolve_model_string(self._model_or_name)
self.check_model(model)
return model
def check_model(self, model):
# a hook that is called at the point that the model argument (which may be a string
# rather than a model class) is resolved to a model class, for subclasses to perform
# any necessary validation checks on that model class
pass
def get_all_permissions_for_user(self, user):
"""
Return a set of all permissions that the given user has on this model.
They may be instances of django.contrib.auth.Permission, or custom
permission objects defined by the policy, which are not necessarily
model instances.
"""
return set()
def get_cached_permissions_for_user(self, user):
"""
Return a list of all permissions that the given user has on this model,
using the cache if available and populating the cache if not.
This can be useful for the other methods to perform efficient queries
against the set of permissions that the user has.
"""
if hasattr(user, self.permission_cache_name):
perms = getattr(user, self.permission_cache_name)
else:
perms = self.get_all_permissions_for_user(user)
if self.permission_cache_name:
setattr(user, self.permission_cache_name, perms)
return perms
# Basic user permission tests. Most policies are expected to override these,
# since the default implementation is to query the set of permitted users
# (which is pretty inefficient).
def user_has_permission(self, user, action):
"""
Return whether the given user has permission to perform the given action
on some or all instances of this model
"""
return user in self.users_with_permission(action)
def user_has_any_permission(self, user, actions):
"""
Return whether the given user has permission to perform any of the given actions
on some or all instances of this model
"""
return any(self.user_has_permission(user, action) for action in actions)
# Operations for retrieving a list of users matching the permission criteria.
# All policies must implement, at minimum, users_with_any_permission.
def users_with_any_permission(self, actions):
"""
Return a queryset of users who have permission to perform any of the given actions
on some or all instances of this model
"""
raise NotImplementedError
def users_with_permission(self, action):
"""
Return a queryset of users who have permission to perform the given action on
some or all instances of this model
"""
return self.users_with_any_permission([action])
# Per-instance permission tests. In the simplest cases - corresponding to the
# basic Django permission model - permissions are enforced on a per-model basis
# and so these methods can simply defer to the per-model tests. Policies that
# require per-instance permission logic must override, at minimum:
# user_has_permission_for_instance
# instances_user_has_any_permission_for
# users_with_any_permission_for_instance
def user_has_permission_for_instance(self, user, action, instance):
"""
Return whether the given user has permission to perform the given action on the
given model instance
"""
return self.user_has_permission(user, action)
def user_has_any_permission_for_instance(self, user, actions, instance):
"""
Return whether the given user has permission to perform any of the given actions
on the given model instance
"""
return any(
self.user_has_permission_for_instance(user, action, instance)
for action in actions
)
def instances_user_has_any_permission_for(self, user, actions):
"""
Return a queryset of all instances of this model for which the given user has
permission to perform any of the given actions
"""
if self.user_has_any_permission(user, actions):
return self.model.objects.all()
else:
return self.model.objects.none()
def instances_user_has_permission_for(self, user, action):
"""
Return a queryset of all instances of this model for which the given user has
permission to perform the given action
"""
return self.instances_user_has_any_permission_for(user, [action])
def users_with_any_permission_for_instance(self, actions, instance):
"""
Return a queryset of all users who have permission to perform any of the given
actions on the given model instance
"""
return self.users_with_any_permission(actions)
def users_with_permission_for_instance(self, action, instance):
return self.users_with_any_permission_for_instance([action], instance)
class BlanketPermissionPolicy(BasePermissionPolicy):
"""
A permission policy that gives everyone (including anonymous users)
full permission over the given model
"""
def user_has_permission(self, user, action):
return True
def user_has_any_permission(self, user, actions):
return True
def users_with_any_permission(self, actions):
# Here we filter out inactive users from the results, even though inactive users
# - and for that matter anonymous users - still have permission according to the
# user_has_permission method. This is appropriate because, for most applications,
# setting is_active=False is equivalent to deleting the user account; you would
# not want these accounts to appear in, for example, a dropdown of users to
# assign a task to. The result here could never be completely logically correct
# (because it will not include anonymous users), so as the next best thing we
# return the "least surprise" result.
return get_user_model().objects.filter(is_active=True)
def users_with_permission(self, action):
return get_user_model().objects.filter(is_active=True)
class AuthenticationOnlyPermissionPolicy(BasePermissionPolicy):
"""
A permission policy that gives all active authenticated users
full permission over the given model
"""
def user_has_permission(self, user, action):
return user.is_authenticated and user.is_active
def user_has_any_permission(self, user, actions):
return user.is_authenticated and user.is_active
def users_with_any_permission(self, actions):
return get_user_model().objects.filter(is_active=True)
def users_with_permission(self, action):
return get_user_model().objects.filter(is_active=True)
class BaseDjangoAuthPermissionPolicy(BasePermissionPolicy):
"""
Extends BasePermissionPolicy with helper methods useful for policies that need to
perform lookups against the django.contrib.auth permission model
"""
def __init__(self, model, auth_model=None):
# `auth_model` specifies the model to be used for permission record lookups;
# usually this will match `model` (which specifies the type of instances that
# `instances_user_has_permission_for` will return), but this may differ when
# swappable models are in use - for example, an interface for editing user
# records might use a custom User model but will typically still refer to the
# permission records for auth.user.
super().__init__(model)
self._auth_model_or_name = auth_model or model
@cached_property
def auth_model(self):
return resolve_model_string(self._auth_model_or_name)
@cached_property
def app_label(self):
return self.auth_model._meta.app_label
@cached_property
def model_name(self):
return self.auth_model._meta.model_name
@cached_property
def _content_type(self):
return ContentType.objects.get_for_model(self.auth_model)
def _get_permission_codenames(self, actions):
return {get_permission_codename(action, self.model._meta) for action in actions}
def _get_permission_name(self, action):
"""
Get the full app-label-qualified permission name (as required by
user.has_perm(...) ) for the given action on this model
"""
return "{}.{}".format(
self.app_label,
get_permission_codename(action, self.model._meta),
)
def _get_permission_objects_for_actions(self, actions):
"""
Get a queryset of the Permission objects for the given actions
"""
return Permission.objects.filter(
content_type=self._content_type,
codename__in=self._get_permission_codenames(actions),
)
def _get_users_with_any_permission_codenames_filter(self, permission_codenames):
"""
Given a list of permission codenames, return a filter expression which
will find all users which have any of those permissions - either
through group permissions, user permissions, or implicitly through
being a superuser.
"""
permissions = Permission.objects.filter(
content_type=self._content_type, codename__in=permission_codenames
)
return (
Q(is_superuser=True)
| Q(user_permissions__in=permissions)
| Q(groups__permissions__in=permissions)
) & Q(is_active=True)
def _get_users_with_any_permission_codenames(self, permission_codenames):
"""
Given a list of permission codenames, return a queryset of users which
have any of those permissions - either through group permissions, user
permissions, or implicitly through being a superuser.
"""
filter_expr = self._get_users_with_any_permission_codenames_filter(
permission_codenames
)
return get_user_model().objects.filter(filter_expr).distinct()
class ModelPermissionPolicy(BaseDjangoAuthPermissionPolicy):
"""
A permission policy that enforces permissions at the model level, by consulting
the standard django.contrib.auth permission model directly
"""
def user_has_permission(self, user, action):
return user.has_perm(self._get_permission_name(action))
def users_with_any_permission(self, actions):
return self._get_users_with_any_permission_codenames(
self._get_permission_codenames(actions)
)
class OwnershipPermissionPolicy(BaseDjangoAuthPermissionPolicy):
"""
A permission policy for objects that support a concept of 'ownership', where
the owner is typically the user who created the object.
This policy piggybacks off 'add' and 'change' permissions defined through the
django.contrib.auth Permission model, as follows:
* any user with 'add' permission can create instances, and ALSO edit instances
that they own
* any user with 'change' permission can edit instances regardless of ownership
* ability to edit also implies ability to delete
Besides 'add', 'change' and 'delete', no other actions are recognised or permitted
(unless the user is an active superuser, in which case they can do everything).
"""
def __init__(self, model, auth_model=None, owner_field_name="owner"):
super().__init__(model, auth_model=auth_model)
self.owner_field_name = owner_field_name
def check_model(self, model):
super().check_model(model)
# make sure owner_field_name is a field that exists on the model
try:
model._meta.get_field(self.owner_field_name)
except FieldDoesNotExist:
raise ImproperlyConfigured(
"%s has no field named '%s'. To use this model with OwnershipPermissionPolicy, "
"you must specify a valid field name as owner_field_name."
% (model, self.owner_field_name)
)
def user_has_permission(self, user, action):
if action == "add":
return user.has_perm(self._get_permission_name("add"))
elif action == "change" or action == "delete":
return (
# having 'add' permission means that there are *potentially*
# some instances they can edit (namely: ones they own),
# which is sufficient for returning True here
user.has_perm(self._get_permission_name("add"))
or user.has_perm(self._get_permission_name("change"))
)
else:
# unrecognised actions are only allowed for active superusers
return user.is_active and user.is_superuser
def users_with_any_permission(self, actions):
if "change" in actions or "delete" in actions:
# either 'add' or 'change' permission means that there are *potentially*
# some instances they can edit
permission_codenames = self._get_permission_codenames({"add", "change"})
elif "add" in actions:
permission_codenames = self._get_permission_codenames({"add"})
else:
# none of the actions passed in here are ones that we recognise, so only
# allow them for active superusers
return get_user_model().objects.filter(is_active=True, is_superuser=True)
return self._get_users_with_any_permission_codenames(permission_codenames)
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):
if "change" in actions or "delete" in actions:
if user.has_perm(self._get_permission_name("change")):
return True
elif (
user.has_perm(self._get_permission_name("add"))
and getattr(instance, self.owner_field_name) == user
):
return True
else:
return False
else:
# 'change' and 'delete' are the only actions that are well-defined
# for specific instances. Other actions are only available to
# active superusers.
return user.is_active and user.is_superuser
def instances_user_has_any_permission_for(self, user, actions):
if user.is_active and user.is_superuser:
# active superusers can perform any action (including unrecognised ones)
# on any instance
return self.model.objects.all()
elif "change" in actions or "delete" in actions:
if user.has_perm(self._get_permission_name("change")):
# user can edit all instances
return self.model.objects.all()
elif user.has_perm(self._get_permission_name("add")):
# user can edit their own instances
return self.model.objects.filter(**{self.owner_field_name: user})
else:
# user has no permissions at all on this model
return self.model.objects.none()
else:
# action is either not recognised, or is the 'add' action which is
# not meaningful for existing instances. As such, non-superusers
# cannot perform it on any existing instances.
return self.model.objects.none()
def users_with_any_permission_for_instance(self, actions, instance):
if "change" in actions or "delete" in actions:
# get filter expression for users with 'change' permission
filter_expr = self._get_users_with_any_permission_codenames_filter(
self._get_permission_codenames({"change"})
)
# add on the item's owner, if they still have 'add' permission
# (and the owner field isn't blank)
owner = getattr(instance, self.owner_field_name)
if owner is not None and owner.has_perm(self._get_permission_name("add")):
filter_expr = filter_expr | Q(pk=owner.pk)
# return the filtered queryset
return get_user_model().objects.filter(filter_expr).distinct()
else:
# action is either not recognised, or is the 'add' action which is
# not meaningful for existing instances. As such, the action is only
# available to superusers
return get_user_model().objects.filter(is_active=True, is_superuser=True)

View File

@@ -0,0 +1,489 @@
from django.contrib.auth import get_permission_codename, get_user_model
from django.contrib.auth.models import Group
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db.models import Q
from wagtail.models import Collection, GroupCollectionPermission
from .base import BaseDjangoAuthPermissionPolicy
class CollectionPermissionLookupMixin:
permission_cache_name = "_collection_permission_cache"
def _get_user_permission_objects_for_actions(self, user, actions):
"""
Get a set of the user's GroupCollectionPermission objects for the given actions
"""
permission_codenames = {
get_permission_codename(action, self.auth_model._meta) for action in actions
}
return {
group_permission
for group_permission in self.get_cached_permissions_for_user(user)
if group_permission.permission.codename in permission_codenames
}
def get_all_permissions_for_user(self, user):
# For these users, we can determine the permissions without querying
# GroupCollectionPermission by checking it directly in _check_perm()
if not user.is_active or user.is_anonymous or user.is_superuser:
return GroupCollectionPermission.objects.none()
return GroupCollectionPermission.objects.filter(
group__user=user
).select_related("permission", "collection")
def _check_perm(self, user, actions, collection=None):
"""
Equivalent to user.has_perm(self._get_permission_name(action)) on all listed actions,
but using GroupCollectionPermission rather than group.permissions.
If collection is specified, only consider GroupCollectionPermission records
that apply to that collection.
"""
if not (user.is_active and user.is_authenticated):
return False
if user.is_superuser:
return True
collection_permissions = self._get_user_permission_objects_for_actions(
user, actions
)
if collection:
collection_permissions = {
permission
for permission in collection_permissions
if collection.is_descendant_of(permission.collection)
or collection.pk == permission.collection_id
}
return bool(collection_permissions)
def _collections_with_perm(self, user, actions):
"""
Return a queryset of collections on which this user has a GroupCollectionPermission
record for any of the given actions, either on the collection itself or an ancestor
"""
permissions = self._get_user_permission_objects_for_actions(user, actions)
collections = Collection.objects.none()
for perm in permissions:
collections |= Collection.objects.descendant_of(
perm.collection, inclusive=True
)
return collections
def _users_with_perm_filter(self, actions, collection=None):
"""
Return a filter expression that will filter a user queryset to those with any
permissions corresponding to 'actions', via either GroupCollectionPermission
or superuser privileges.
If collection is specified, only consider GroupCollectionPermission records
that apply to that collection.
"""
permissions = self._get_permission_objects_for_actions(actions)
# Find all groups with GroupCollectionPermission records for
# any of these permissions
groups = Group.objects.filter(
collection_permissions__permission__in=permissions,
)
if collection is not None:
collections = collection.get_ancestors(inclusive=True)
groups = groups.filter(collection_permissions__collection__in=collections)
# Find all users who are active, and are superusers or in any of these groups
return Q(is_active=True) & (Q(is_superuser=True) | Q(groups__in=groups))
def _users_with_perm(self, actions, collection=None):
"""
Return a queryset of users with any permissions corresponding to 'actions',
via either GroupCollectionPermission or superuser privileges.
If collection is specified, only consider GroupCollectionPermission records
that apply to that collection.
"""
return (
get_user_model()
.objects.filter(
self._users_with_perm_filter(actions, collection=collection)
)
.distinct()
)
def collections_user_has_any_permission_for(self, user, actions):
"""
Return a queryset of all collections in which the given user has
permission to perform any of the given actions
"""
if user.is_active and user.is_superuser:
# active superusers can perform any action (including unrecognised ones)
# in any collection
return Collection.objects.all()
if not user.is_authenticated:
return Collection.objects.none()
return self._collections_with_perm(user, actions)
def collections_user_has_permission_for(self, user, action):
"""
Return a queryset of all collections in which the given user has
permission to perform the given action
"""
return self.collections_user_has_any_permission_for(user, [action])
class CollectionPermissionPolicy(
CollectionPermissionLookupMixin, BaseDjangoAuthPermissionPolicy
):
"""
A permission policy for objects that are assigned locations in the Collection tree.
Permissions may be defined at any node of the hierarchy, through the
GroupCollectionPermission model, and propagate downwards. These permissions are
applied to objects according to the standard django.contrib.auth permission model.
"""
def user_has_permission(self, user, action):
"""
Return whether the given user has permission to perform the given action
on some or all instances of this model
"""
return self._check_perm(user, [action])
def user_has_any_permission(self, user, actions):
"""
Return whether the given user has permission to perform any of the given actions
on some or all instances of this model
"""
return self._check_perm(user, actions)
def users_with_any_permission(self, actions):
"""
Return a queryset of users who have permission to perform any of the given actions
on some or all instances of this model
"""
return self._users_with_perm(actions)
def user_has_permission_for_instance(self, user, action, instance):
"""
Return whether the given user has permission to perform the given action on the
given model instance
"""
return self._check_perm(user, [action], collection=instance.collection)
def user_has_any_permission_for_instance(self, user, actions, instance):
"""
Return whether the given user has permission to perform any of the given actions
on the given model instance
"""
return self._check_perm(user, actions, collection=instance.collection)
def instances_user_has_any_permission_for(self, user, actions):
"""
Return a queryset of all instances of this model for which the given user has
permission to perform any of the given actions
"""
if not (user.is_active and user.is_authenticated):
return self.model.objects.none()
elif user.is_superuser:
return self.model.objects.all()
else:
# filter to just the collections with this permission
return self.model.objects.filter(
collection__in=list(self._collections_with_perm(user, actions))
)
def users_with_any_permission_for_instance(self, actions, instance):
"""
Return a queryset of all users who have permission to perform any of the given
actions on the given model instance
"""
return self._users_with_perm(actions, collection=instance.collection)
class CollectionOwnershipPermissionPolicy(
CollectionPermissionLookupMixin, BaseDjangoAuthPermissionPolicy
):
"""
A permission policy for objects that are assigned locations in the Collection tree.
Permissions may be defined at any node of the hierarchy, through the
GroupCollectionPermission model, and propagate downwards. These permissions are
applied to objects according to the 'ownership' permission model
(see permission_policies.base.OwnershipPermissionPolicy)
"""
def __init__(self, model, auth_model=None, owner_field_name="owner"):
super().__init__(model, auth_model=auth_model)
self.owner_field_name = owner_field_name
def check_model(self, model):
super().check_model(model)
# make sure owner_field_name is a field that exists on the model
try:
model._meta.get_field(self.owner_field_name)
except FieldDoesNotExist:
raise ImproperlyConfigured(
"%s has no field named '%s'. To use this model with "
"CollectionOwnershipPermissionPolicy, you must specify a valid field name as "
"owner_field_name." % (model, self.owner_field_name)
)
def user_has_permission(self, user, action):
if action == "add":
return self._check_perm(user, ["add"])
elif action == "choose":
return self._check_perm(user, ["choose"])
elif action == "change" or action == "delete":
# having 'add' permission means that there are *potentially*
# some instances they can edit (namely: ones they own),
# which is sufficient for returning True here
return self._check_perm(user, ["add", "change"])
else:
# unrecognised actions are only allowed for active superusers
return user.is_active and user.is_superuser
def users_with_any_permission(self, actions):
known_actions = set(actions) & {"add", "choose", "change"}
# "delete" is considered equivalent to "change"
if "delete" in actions:
known_actions.add("change")
# users with only "add" permission can still change instances they own
if "change" in known_actions:
known_actions.add("add")
if not known_actions:
# none of the actions passed in here are ones that we recognise, so only
# allow them for active superusers
return get_user_model().objects.filter(is_active=True, is_superuser=True)
return self._users_with_perm(known_actions)
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):
known_actions = set(actions) & {"add", "choose", "change"}
# "delete" is considered equivalent to "change"
if "delete" in actions:
known_actions.add("change")
# users with only "add" permission can still change instances they own
if (
"change" in known_actions
and getattr(instance, self.owner_field_name) == user
):
known_actions.add("add")
if known_actions:
return self._check_perm(user, known_actions, collection=instance.collection)
else:
# 'change', 'delete', and 'choose' are the only actions that are well-defined
# for specific instances. Other actions are only available to
# active superusers.
return user.is_active and user.is_superuser
def instances_user_has_any_permission_for(self, user, actions):
known_actions = set(actions) & {"change", "choose"}
# "delete" is considered equivalent to "change"
if "delete" in actions:
known_actions.add("change")
if user.is_active and user.is_superuser:
# active superusers can perform any action (including unrecognised ones)
# on any instance
return self.model.objects.all()
elif not user.is_authenticated:
return self.model.objects.none()
elif known_actions:
# if "change" or "delete" in actions, return instances which are:
# - in (a descendant of) a collection for which they have "change" permission
# - OR in (a descendant of) a collection for which they have "add" permission,
# and are owned by them
# if "choose" in actions, return instances which are:
# - in (a descendant of) a collection for which they have "choose" permission.
# Note that if the actions contain both cases, the results will be combined
# because the user has "any" of the permissions in the actions.
collections = self._collections_with_perm(user, known_actions)
perm_filter = Q(collection__in=collections)
# "add" permission implies "change" permission,
# but only if the instance is owned by the user
if "change" in known_actions:
perm_filter |= Q(
collection__in=self._collections_with_perm(user, ["add"])
) & Q(**{self.owner_field_name: user})
return self.model.objects.filter(perm_filter)
else:
# action is either not recognised, or is the 'add' action which is
# not meaningful for existing instances. As such, non-superusers
# cannot perform it on any existing instances.
return self.model.objects.none()
def users_with_any_permission_for_instance(self, actions, instance):
known_actions = set(actions) & {"choose", "change"}
# "delete" is considered equivalent to "change"
if "delete" in actions:
known_actions.add("change")
filter_expr = self._users_with_perm_filter(
known_actions, collection=instance.collection
)
# users with only "add" permission can still change instances they own
if "change" in known_actions:
owner = getattr(instance, self.owner_field_name)
if owner is not None and self._check_perm(
owner, {"add"}, collection=instance.collection
):
filter_expr |= Q(pk=owner.pk)
if known_actions:
return get_user_model().objects.filter(filter_expr).distinct()
else:
# action is either not recognised, or is the 'add' action which is
# not meaningful for existing instances. As such, the action is only
# available to superusers
return get_user_model().objects.filter(is_active=True, is_superuser=True)
def collections_user_has_any_permission_for(self, user, actions):
"""
Return a queryset of all collections in which the given user has
permission to perform any of the given actions
"""
known_actions = set(actions) & {"add", "choose", "change"}
# "delete" is considered equivalent to "change"
if "delete" in actions:
known_actions.add("change")
# users with only "add" permission can still change instances they own
if "change" in known_actions:
known_actions.add("add")
if user.is_active and user.is_superuser:
# active superusers can perform any action (including unrecognised ones)
# in any collection
return Collection.objects.all()
elif not user.is_authenticated:
return Collection.objects.none()
elif known_actions:
return self._collections_with_perm(user, known_actions)
else:
# action is not recognised, and so non-superusers
# cannot perform it on any existing collections
return Collection.objects.none()
class CollectionManagementPermissionPolicy(
CollectionPermissionLookupMixin, BaseDjangoAuthPermissionPolicy
):
def _descendants_with_perm(self, user, action):
"""
Return a queryset of collections descended from a collection on which this user has
a GroupCollectionPermission record for this action. Used for actions, like edit and
delete where the user cannot modify the collection where they are granted permission.
"""
# Get the permission object corresponding to this action
permission = self._get_permission_objects_for_actions([action]).first()
# Get the collections that have a GroupCollectionPermission record
# for this permission and any of the user's groups;
# create a list of their paths
collection_roots = Collection.objects.filter(
group_permissions__group__in=user.groups.all(),
group_permissions__permission=permission,
).values("path", "depth")
if collection_roots:
# build a filter expression that will filter our model to just those
# instances in collections with a path that starts with one of the above
# but excluding the collection on which permission was granted
collection_path_filter = Q(
path__startswith=collection_roots[0]["path"]
) & Q(depth__gt=collection_roots[0]["depth"])
for collection in collection_roots[1:]:
collection_path_filter = collection_path_filter | (
Q(path__startswith=collection["path"])
& Q(depth__gt=collection["depth"])
)
return Collection.objects.all().filter(collection_path_filter)
else:
# no matching collections
return Collection.objects.none()
def user_has_permission(self, user, action):
"""
Return whether the given user has permission to perform the given action
on some or all instances of this model
"""
return self.user_has_any_permission(user, [action])
def user_has_any_permission(self, user, actions):
"""
Return whether the given user has permission to perform any of the given actions
on some or all instances of this model.
"""
return self._check_perm(user, actions)
def users_with_any_permission(self, actions):
"""
Return a queryset of users who have permission to perform any of the given actions
on some or all instances of this model
"""
return self._users_with_perm(actions)
def user_has_permission_for_instance(self, user, action, instance):
"""
Return whether the given user has permission to perform the given action on the
given model instance
"""
return self._check_perm(user, [action], collection=instance)
def user_has_any_permission_for_instance(self, user, actions, instance):
"""
Return whether the given user has permission to perform any of the given actions
on the given model instance
"""
return self._check_perm(user, actions, collection=instance)
def users_with_any_permission_for_instance(self, actions, instance):
"""
Return a queryset of all users who have permission to perform any of the given
actions on the given model instance
"""
return self._users_with_perm(actions, collection=instance)
def instances_user_has_permission_for(self, user, action):
if user.is_active and user.is_superuser:
# active superusers can perform any action (including unrecognised ones)
# in any collection - except for deleting the root collection
if action == "delete":
return Collection.objects.exclude(depth=1).all()
else:
return Collection.objects.all()
elif not user.is_authenticated:
return Collection.objects.none()
else:
if action == "delete":
return self._descendants_with_perm(user, action)
else:
return self._collections_with_perm(user, [action])
def instances_user_has_any_permission_for(self, user, actions):
return self.collections_user_has_any_permission_for(user, actions)

View File

@@ -0,0 +1,215 @@
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"}
)
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"}
)
# 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