Ejemplo n.º 1
0
    def remove_perm(self, user_obj, perm, defer_recalc=False, skip_kc=False):
        """
            Revoke the given `perm` on this object from `user_obj`. By default,
            recalculate descendant objects' permissions and remove any
            applicable KC permissions.  May delete granted permissions or add
            deny permissions as appropriate:
            Current access      Action
            ==============      ======
            None                None
            Direct              Remove direct permission
            Inherited           Add deny permission
            Direct & Inherited  Remove direct permission; add deny permission
            :type user_obj: :py:class:`User` or :py:class:`AnonymousUser`
            :param perm str: The `codename` of the `Permission`
            :param defer_recalc bool: When `True`, skip recalculating
                descendants
            :param skip_kc bool: When `True`, skip assignment of applicable KC
                permissions
        """
        if isinstance(user_obj, AnonymousUser):
            # Get the User database representation for AnonymousUser
            user_obj = get_anonymous_user()
        app_label, codename = perm_parse(perm, self)
        # Get all assignable permissions, regardless of asset type. That way,
        # we can allow invalid permissions to be removed
        if codename not in self.get_assignable_permissions(ignore_type=True):
            # Some permissions are calculated and not stored in the database
            raise serializers.ValidationError(
                {'permission': f'{codename} cannot be removed explicitly.'})
        all_permissions = self.permissions.filter(
            user=user_obj, permission__codename=codename, deny=False)
        direct_permissions = all_permissions.filter(inherited=False)
        inherited_permissions = all_permissions.filter(inherited=True)
        # Resolve implied permissions, e.g. revoking view implies revoking
        # change
        implied_perms = self.get_implied_perms(codename,
                                               reverse=True,
                                               for_instance=self)
        for implied_perm in implied_perms:
            self.remove_perm(user_obj, implied_perm, defer_recalc=True)
        # Delete directly assigned permissions, if any
        direct_permissions.delete()
        if inherited_permissions.exists():
            # Delete inherited permissions
            inherited_permissions.delete()
            # Add a deny permission to block future inheritance
            self.assign_perm(user_obj, perm, deny=True, defer_recalc=True)
        # Remove any applicable KC permissions
        if not skip_kc:
            remove_applicable_kc_permissions(self, user_obj, codename)

        # We might have been called by ourself to assign a related
        # permission. In that case, don't recalculate here.
        if defer_recalc:
            return

        self._update_partial_permissions(user_obj.pk, perm, remove=True)
        # Recalculate all descendants
        self.recalculate_descendants_perms()
Ejemplo n.º 2
0
    def assign_perm(self,
                    user_obj,
                    perm,
                    deny=False,
                    defer_recalc=False,
                    skip_kc=False,
                    partial_perms=None):
        r"""
            Assign `user_obj` the given `perm` on this object, or break
            inheritance from a parent object. By default, recalculate
            descendant objects' permissions and apply any applicable KC
            permissions.
            :type user_obj: :py:class:`User` or :py:class:`AnonymousUser`
            :param perm: str. The `codename` of the `Permission`
            :param deny: bool. When `True`, break inheritance from parent object
            :param defer_recalc: bool. When `True`, skip recalculating
                descendants
            :param skip_kc: bool. When `True`, skip assignment of applicable KC
                permissions
            :param partial_perms: dict. Filters used to narrow down query for
              partial permissions
        """
        app_label, codename = perm_parse(perm, self)
        if codename not in self.get_assignable_permissions():
            # Some permissions are calculated and not stored in the database
            raise ValidationError(
                '{} cannot be assigned explicitly to {} objects.'.format(
                    codename, self._meta.model_name))
        if isinstance(user_obj,
                      AnonymousUser) or (user_obj.pk
                                         == settings.ANONYMOUS_USER_ID):
            # Is an anonymous user allowed to have this permission?
            fq_permission = f'{app_label}.{codename}'
            if (not deny and fq_permission
                    not in settings.ALLOWED_ANONYMOUS_PERMISSIONS):
                raise ValidationError(
                    'Anonymous users cannot be granted the permission {}.'.
                    format(codename))
            # Get the User database representation for AnonymousUser
            user_obj = get_anonymous_user()
        perm_model = Permission.objects.get(content_type__app_label=app_label,
                                            codename=codename)
        existing_perms = ObjectPermission.objects.filter_for_object(
            self,
            user=user_obj,
        )
        identical_existing_perm = existing_perms.filter(
            inherited=False,
            permission_id=perm_model.pk,
            deny=deny,
        )
        if identical_existing_perm.exists():
            # We need to always update partial permissions because
            # they may have changed even if `perm` is the same.
            self._update_partial_permissions(user_obj.pk,
                                             perm,
                                             partial_perms=partial_perms)
            # The user already has this permission directly applied
            return identical_existing_perm.first()

        # Remove any explicitly-defined contradictory grants or denials
        contradictory_filters = models.Q(user=user_obj,
                                         permission_id=perm_model.pk,
                                         deny=not deny,
                                         inherited=False)
        if not deny and perm in self.CONTRADICTORY_PERMISSIONS.keys():
            contradictory_filters |= models.Q(
                user=user_obj,
                permission__codename__in=self.CONTRADICTORY_PERMISSIONS.get(
                    perm),
            )
        contradictory_perms = existing_perms.filter(contradictory_filters)
        contradictory_codenames = list(
            contradictory_perms.values_list('permission__codename', flat=True))

        contradictory_perms.delete()
        # Check if any KC permissions should be removed as well
        if deny and not skip_kc:
            remove_applicable_kc_permissions(self, user_obj,
                                             contradictory_codenames)
        # Create the new permission
        new_permission = ObjectPermission.objects.create(
            content_object=self,
            user=user_obj,
            permission_id=perm_model.pk,
            deny=deny,
            inherited=False)
        # Assign any applicable KC permissions
        if not deny and not skip_kc:
            assign_applicable_kc_permissions(self, user_obj, codename)
        # Resolve implied permissions, e.g. granting change implies granting
        # view
        implied_perms = self.get_implied_perms(codename, reverse=deny)
        for implied_perm in implied_perms:
            self.assign_perm(user_obj,
                             implied_perm,
                             deny=deny,
                             defer_recalc=True)
        # We might have been called by ourself to assign a related
        # permission. In that case, don't recalculate here.
        if defer_recalc:
            return new_permission

        self._update_partial_permissions(user_obj.pk,
                                         perm,
                                         partial_perms=partial_perms)

        # Recalculate all descendants, re-fetching ourself first to guard
        # against stale MPTT values
        fresh_self = type(self).objects.get(pk=self.pk)
        fresh_self.recalculate_descendants_perms()
        return new_permission
Ejemplo n.º 3
0
    def copy_permissions_from(self, source_object):
        """
        Copies permissions from `source_object` to `self` object.
        Both objects must have the same type.

        :param source_object: Asset
        :return: Boolean
        """

        # We can only copy permissions between objects from the same type.
        if type(source_object) is type(self):
            # First delete all permissions of the target asset (except owner's).
            perm_queryset = self.permissions.exclude(user_id=self.owner_id)
            # The bulk delete below (i.e.: `perm_queryset.delete()`) does not
            # remove permissions in KoBoCAT.
            # We could loop through `self.permissions` and call `remove_perm`
            # for each permission but it would have probably a performance hit
            # with assets with lots of permissions.
            # Let's use PostgreSQL specific function `ArrayAgg` to retrieve all
            # codenames at once.

            # It relies on the fact that permissions are synced in KoBoCAT and KPI.
            # If any permissions are present in KoBoCAT but not in KPI, these
            # permissions will not be deleted and will have to be deleted manually
            # with KoBoCAT.

            user_codenames = (ObjectPermission.objects.filter(
                asset_id=self.pk, deny=False).exclude(
                    user_id=self.owner_id).values('user_id').annotate(
                        all_codenames=ArrayAgg('permission__codename',
                                               distinct=True)))
            for user_codename in user_codenames:
                remove_applicable_kc_permissions(
                    self, user_codename['user_id'],
                    user_codename['all_codenames'])

            # Remove all permissions from the asset (except the owner's)
            perm_queryset.delete()

            # Then copy all permissions from source to target asset
            source_permissions = list(source_object.permissions.all())
            for source_permission in source_permissions:
                # Only need to reassign permissions if user is not the owner
                if source_permission.user_id != self.owner_id:
                    kwargs = {
                        'user_obj': source_permission.user,
                        'perm': source_permission.permission.codename,
                        'deny': source_permission.deny
                    }
                    if source_permission.permission.codename.startswith(
                            PREFIX_PARTIAL_PERMS):
                        kwargs.update({
                            'partial_perms':
                            source_object.get_partial_perms(
                                source_permission.user_id, with_filters=True)
                        })
                    self.assign_perm(**kwargs)
            self._recalculate_inherited_perms()
            return True
        else:
            return False