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()
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
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