489 lines
20 KiB
Python
489 lines
20 KiB
Python
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)
|