def _get_subscribed(cls, user): # Of the public objects, determine to which the user has subscribed if is_user_anonymous(user): user = get_anonymous_user() return UserAssetSubscription.objects.filter( asset__in=cls._get_publics(), user=user).values('asset')
def get_user_permission_assignments( affected_object, user, object_permission_assignments ): """ Works like `get_user_permission_assignments_queryset` but returns a list instead of a queryset. It also needs a list of all `affected_object`'s permission assignments to search for assignments `user` is allowed to see. Args: affected_object (Asset) user (User) object_permission_assignments (list): Returns: list """ user_permission_assignments = [] filtered_user_ids = None if not user or is_user_anonymous(user): filtered_user_ids = [affected_object.owner_id] elif not affected_object.has_perm(user, PERM_MANAGE_ASSET): # Display only users' permissions if they are not allowed to modify # others' permissions filtered_user_ids = [affected_object.owner_id, user.pk, settings.ANONYMOUS_USER_ID] for permission_assignment in object_permission_assignments: if (filtered_user_ids is None or permission_assignment.user_id in filtered_user_ids): user_permission_assignments.append(permission_assignment) return user_permission_assignments
def _get_queryset_for_data_sharing_enabled(self, request: Request, queryset: QuerySet) -> QuerySet: """ Returns a queryset containing the assets that the user is allowed to view data. I.e., user needs 'view_submissions' or 'partial_submissions'. """ self._return_queryset = False parameters = self.__get_parsed_parameters(request) try: data_sharing = parameters[self.DATA_SHARING_PARAMETER][0] except KeyError: return queryset if not data_sharing: # No reason to be False, but just in case return queryset self._return_queryset = True required_perm_ids = get_perm_ids_from_code_names( [PERM_VIEW_SUBMISSIONS, PERM_PARTIAL_SUBMISSIONS]) user = request.user if is_user_anonymous(user): # Avoid giving anonymous users special treatment when viewing # public objects perms = ObjectPermission.objects.none() else: perms = ObjectPermission.objects.filter( deny=False, user=user, permission_id__in=required_perm_ids) asset_ids = perms.values('asset') # `SearchFilter` handles further filtering to include only assets with # `data_sharing__enabled` (`self.DATA_SHARING_PARAMETER`) set to true return queryset.filter(pk__in=asset_ids)
def _get_owned_and_explicitly_shared(user): view_asset_perm_id = get_perm_ids_from_code_names(PERM_VIEW_ASSET) if is_user_anonymous(user): # Avoid giving anonymous users special treatment when viewing # public objects perms = ObjectPermission.objects.none() else: perms = ObjectPermission.objects.filter( deny=False, user=user, permission_id=view_asset_perm_id) return perms.values('asset')
def assign_applicable_kc_permissions( obj: Model, user: Union[AnonymousUser, User, int], kpi_codenames: Union[str, list] ): """ Assign the `user` the applicable KC permissions to `obj`, if any exists, given one KPI permission codename as a single string or many codenames as an iterable. If `obj` is not a :py:class:`Asset` or does not have a deployment, take no action. """ if not obj._meta.model_name == 'asset': return permissions = _get_applicable_kc_permissions(obj, kpi_codenames) if not permissions: return xform_id = _get_xform_id_for_asset(obj) if not xform_id: return # Retrieve primary key from user object and use it on subsequent queryset. # It avoids loading the object when `user` is passed as an integer. if not isinstance(user, int): if is_user_anonymous(user): user_id = settings.ANONYMOUS_USER_ID else: user_id = user.pk else: user_id = user if user_id == settings.ANONYMOUS_USER_ID: return set_kc_anonymous_permissions_xform_flags( obj, kpi_codenames, xform_id) xform_content_type = KobocatContentType.objects.get( **obj.KC_CONTENT_TYPE_KWARGS) kc_permissions_already_assigned = KobocatUserObjectPermission.objects.filter( user_id=user.pk, permission__in=permissions, object_pk=xform_id, ).values_list('permission__codename', flat=True) permissions_to_create = [] for permission in permissions: if permission.codename in kc_permissions_already_assigned: continue permissions_to_create.append(KobocatUserObjectPermission( user_id=user.pk, permission=permission, object_pk=xform_id, content_type=xform_content_type )) KobocatUserObjectPermission.objects.bulk_create(permissions_to_create)
def __get_object_permissions(self, deny, user=None, codename=None): """ Returns a set of user ids and object permission ids related to object `self`. Args: deny (bool): If `True`, returns denied permissions user (User) codename (str) Returns: set: [(User's pk, Permission's pk)] """ def build_dict(user_id_, object_permissions_): perms_ = [] if object_permissions_: for permission_id, codename_, deny_ in object_permissions_: if (deny_ is not deny or codename is not None and codename != codename_): continue perms_.append((user_id_, permission_id)) return perms_ perms = [] # If User is not none, retrieve all permissions for this user # grouped by object ids, otherwise, retrieve all permissions for # this object grouped by user ids. if user is not None: # Ensuring that the user has at least anonymous permissions if they # have been assigned to the asset all_anon_object_permissions = self.__get_all_user_permissions( user_id=settings.ANONYMOUS_USER_ID) perms = build_dict( settings.ANONYMOUS_USER_ID, all_anon_object_permissions.get(self.pk), ) if not is_user_anonymous(user): all_object_permissions = self.__get_all_user_permissions( user_id=user.pk) perms += build_dict(user.pk, all_object_permissions.get(self.pk)) else: all_object_permissions = self.__get_all_object_permissions( object_id=self.pk) for user_id, object_permissions in all_object_permissions.items(): perms += build_dict(user_id, object_permissions) return set(perms)
def remove_applicable_kc_permissions( obj: Model, user: Union[AnonymousUser, User, int], kpi_codenames: Union[str, list] ): """ Remove the `user` the applicable KC permissions from `obj`, if any exists, given one KPI permission codename as a single string or many codenames as an iterable. If `obj` is not a :py:class:`Asset` or does not have a deployment, take no action. """ if not obj._meta.model_name == 'asset': return permissions = _get_applicable_kc_permissions(obj, kpi_codenames) if not permissions: return xform_id = _get_xform_id_for_asset(obj) if not xform_id: return # Retrieve primary key from user object and use it on subsequent queryset. # It avoids loading the object when `user` is passed as an integer. if not isinstance(user, int): if is_user_anonymous(user): user_id = settings.ANONYMOUS_USER_ID else: user_id = user.pk else: user_id = user if user_id == settings.ANONYMOUS_USER_ID: return set_kc_anonymous_permissions_xform_flags( obj, kpi_codenames, xform_id, remove=True) content_type_kwargs = _get_content_type_kwargs_for_related(obj) KobocatUserObjectPermission.objects.filter( user_id=user_id, permission__in=permissions, object_pk=xform_id, # `permission` has a FK to `ContentType`, but I'm paranoid **content_type_kwargs ).delete()
def get_user_permission_assignments_queryset(affected_object, user): """ Returns a queryset to fetch `affected_object`'s permission assignments that `user` is allowed to see. Args: affected_object (Asset) user (User) Returns: QuerySet """ # `affected_object.permissions` is a `GenericRelation(ObjectPermission)` # Don't Prefetch `content_object`. # See `AssetPermissionAssignmentSerializer.to_representation()` queryset = affected_object.permissions.filter(deny=False).select_related( 'permission', 'user' ).order_by( 'user__username', 'permission__codename' ).exclude(permission__codename=PERM_FROM_KC_ONLY).all() # Filtering is done in `get_queryset` instead of FilteredBackend class # because it's specific to `ObjectPermission`. if not user or is_user_anonymous(user): queryset = queryset.filter( user_id__in=[ affected_object.owner_id, settings.ANONYMOUS_USER_ID, ] ) elif not affected_object.has_perm(user, PERM_MANAGE_ASSET): # Display only users' permissions if they are not allowed to modify # others' permissions queryset = queryset.filter(user_id__in=[user.pk, affected_object.owner_id, settings.ANONYMOUS_USER_ID]) return queryset
def has_perm(self, user_obj: User, perm: str) -> bool: """ Does `user_obj` have perm on this object? (True/False) """ app_label, codename = perm_parse(perm, self) is_anonymous = is_user_anonymous(user_obj) user_obj = get_database_user(user_obj) # Treat superusers the way django.contrib.auth does if user_obj.is_active and user_obj.is_superuser: return True # Look for matching permissions result = len( self._get_effective_perms(user=user_obj, codename=codename)) == 1 if not result and not is_anonymous: # The user-specific test failed, but does the public have access? result = self.has_perm(AnonymousUser(), perm) if result and is_anonymous: # Is an anonymous user allowed to have this permission? fq_permission = '{}.{}'.format(app_label, codename) if fq_permission not in settings.ALLOWED_ANONYMOUS_PERMISSIONS: return False return result
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) assignable_permissions = self.get_assignable_permissions() if codename not in assignable_permissions: # Some permissions are calculated and not stored in the database raise serializers.ValidationError({ 'permission': f'{codename} cannot be assigned explicitly to {self}' }) is_anonymous = is_user_anonymous(user_obj) user_obj = get_database_user(user_obj) if is_anonymous: # 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 serializers.ValidationError({ 'permission': f'Anonymous users cannot be granted the permission {codename}.' }) perm_model = Permission.objects.get(content_type__app_label=app_label, codename=codename) existing_perms = self.permissions.filter(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, 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( asset=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_instance=self).intersection(assignable_permissions) for implied_perm in implied_perms: self.assign_perm(user_obj, implied_perm, deny=deny, defer_recalc=True) # We might have been called by ourselves to assign a related # permission. In that case, don't recalculate here. if defer_recalc: return new_permission self._update_partial_permissions(user_obj, perm, partial_perms=partial_perms) # Recalculate all descendants self.recalculate_descendants_perms() return new_permission