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)