Beispiel #1
0
    def check_object_permissions(self, request, obj):
        # If the instance is marked as deleted and the client is not allowed to
        # see deleted instances, we want to return a 404, behaving as if it
        # does not exist.
        if obj.deleted and not GroupPermission(
            amo.permissions.ADDONS_VIEW_DELETED
        ).has_object_permission(request, self, obj):
            raise http.Http404

        if obj.channel == amo.RELEASE_CHANNEL_UNLISTED:
            # If the instance is unlisted, only allow unlisted reviewers and
            # authors..
            self.permission_classes = [
                AllowRelatedObjectPermissions(
                    'addon', [AnyOf(AllowUnlistedViewerOrReviewer, AllowAddonAuthor)]
                )
            ]
        elif not obj.is_public():
            # If the instance is disabled, only allow reviewers and authors.
            self.permission_classes = [
                AllowRelatedObjectPermissions(
                    'addon', [AnyOf(AllowListedViewerOrReviewer, AllowAddonAuthor)]
                )
            ]
        super(AddonVersionViewSet, self).check_object_permissions(request, obj)
Beispiel #2
0
class TestAllowRelatedObjectPermissions(TestCase):
    def setUp(self):
        self.permission = AllowRelatedObjectPermissions(
            'test_property', [AllowOwner, AllowAny])
        self.allowed_user = user_factory()
        self.related_obj = Mock(user=self.allowed_user)
        self.obj = Mock(test_property=self.related_obj)
        self.request = RequestFactory().post('/')
        self.request.user = self.allowed_user

    def test_all_must_pass(self):
        assert self.permission.has_permission(
            self.request, 'myview') is True

        self.request.user = AnonymousUser()
        assert self.permission.has_permission(
            self.request, 'myview') is False

    def test_all_must_pass_object(self):
        assert self.permission.has_object_permission(
            self.request, 'myview', self.obj) is True

        self.request.user = AnonymousUser()
        assert self.permission.has_permission(
            self.request, 'myview') is False
 def setUp(self):
     self.permission = AllowRelatedObjectPermissions(
         'test_property', [AllowOwner, AllowAny])
     self.allowed_user = user_factory()
     self.related_obj = Mock(user=self.allowed_user)
     self.obj = Mock(test_property=self.related_obj)
     self.request = RequestFactory().post('/')
     self.request.user = self.allowed_user
Beispiel #4
0
 def setUp(self):
     self.permission = AllowRelatedObjectPermissions(
         'test_property', [AllowOwner, AllowAny])
     self.allowed_user = user_factory()
     self.related_obj = Mock(user=self.allowed_user)
     self.obj = Mock(test_property=self.related_obj)
     self.request = RequestFactory().post('/')
     self.request.user = self.allowed_user
Beispiel #5
0
class AddonVersionViewSet(AddonChildMixin, RetrieveModelMixin, ListModelMixin,
                          GenericViewSet):
    # Permissions are checked against the parent add-on.
    permission_classes = [
        AllowRelatedObjectPermissions('addon', AddonViewSet.permission_classes)
    ]
    serializer_class = VersionSerializer
    # Since permission checks are done on the parent add-on, we rely on
    # queryset filtering to hide non-valid versions. get_queryset() might
    # override this if we are asked to see non-valid versions explicitly.
    queryset = Version.objects.filter(
        files__status__in=amo.VALID_FILE_STATUSES).distinct()

    def get_queryset(self):
        """Return the right base queryset depending on the situation. Note that
        permissions checks still apply on top of that, against the add-on
        as per check_object_permissions() above."""
        requested = self.request.GET.get('filter')

        # By default we restrict to valid versions. However:
        #
        # When accessing a single version or if requesting it explicitly when
        # listing, admins can access all versions, including deleted ones.
        should_access_all_versions_included_deleted = (
            (requested == 'all_with_deleted' or self.action != 'list')
            and self.request.user.is_authenticated()
            and self.request.user.is_staff)

        # When accessing a single version or if requesting it explicitly when
        # listing, reviewers and add-on authors can access all non-deleted
        # versions.
        should_access_all_versions = (
            (requested == 'all' or self.action != 'list')
            and (AllowReviewer().has_permission(self.request, self)
                 or AllowAddonAuthor().has_object_permission(
                     self.request, self, self.get_addon_object())))

        # Everyone can see (non deleted) beta version when they request it
        # explicitly.
        should_access_only_beta_versions = (requested == 'beta_only')

        if should_access_all_versions_included_deleted:
            self.queryset = Version.unfiltered.all()
        elif should_access_all_versions:
            self.queryset = Version.objects.all()
        elif should_access_only_beta_versions:
            self.queryset = Version.objects.filter(
                files__status=amo.STATUS_BETA).distinct()

        # Now that the base queryset has been altered, call super() to use it.
        qs = super(AddonVersionViewSet, self).get_queryset()
        # Filter with the add-on.
        return qs.filter(addon=self.get_addon_object())
class TestAllowRelatedObjectPermissions(TestCase):
    def setUp(self):
        self.permission = AllowRelatedObjectPermissions(
            'test_property', [AllowOwner, AllowAny])
        self.allowed_user = user_factory()
        self.related_obj = Mock(user=self.allowed_user)
        self.obj = Mock(test_property=self.related_obj)
        self.request = RequestFactory().post('/')
        self.request.user = self.allowed_user

    def test_all_must_pass(self):
        assert self.permission.has_permission(self.request, 'myview') is True

        self.request.user = AnonymousUser()
        assert self.permission.has_permission(self.request, 'myview') is False

    def test_all_must_pass_object(self):
        assert self.permission.has_object_permission(self.request, 'myview',
                                                     self.obj) is True

        self.request.user = AnonymousUser()
        assert self.permission.has_permission(self.request, 'myview') is False
Beispiel #7
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get': AllowAny,
            'head': AllowAny,
            'options': AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete': CanDeleteRatingPermission,

            # To post a rating you just need to be authenticated.
            'post': IsAuthenticated,

            # To edit a rating you need to be the author or be an admin.
            'patch': AnyOf(AllowOwner, GroupPermission(
                amo.permissions.ADDONS_EDIT)),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [AnyOf(
        GroupPermission(amo.permissions.ADDONS_EDIT),
        AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
    )]
    reply_serializer_class = RatingSerializerReply
    throttle_classes = (RatingThrottle,)

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = (
                self.request.data.get('addon') or
                self.request.GET.get('addon'))
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_text(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(RatingViewSet, self).get_addon_object(
            permission_classes=[AllowIfPublic])

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(RatingViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(RatingViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
        return super(RatingViewSet, self).filter_queryset(qs)

    def get_paginated_response(self, data):
        response = super(RatingViewSet, self).get_paginated_response(data)
        if 'show_grouped_ratings' in self.request.GET:
            try:
                show_grouped_ratings = (
                    serializers.BooleanField().to_internal_value(
                        self.request.GET['show_grouped_ratings']))
            except serializers.ValidationError:
                raise ParseError(
                    'show_grouped_ratings parameter should be a boolean')
            if show_grouped_ratings and self.get_addon_object():
                response.data['grouped_ratings'] = dict(GroupedRating.get(
                    self.addon_object.id))
        return response

    def get_queryset(self):
        requested = self.request.GET.get('filter', '').split(',')
        has_addons_edit = acl.action_allowed(self.request,
                                             amo.permissions.ADDONS_EDIT)

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        if not hasattr(self, 'should_access_deleted_ratings'):
            self.should_access_deleted_ratings = (
                ('with_deleted' in requested or self.action != 'list') and
                self.request.user.is_authenticated() and
                has_addons_edit)

        should_access_only_top_level_ratings = (
            self.action == 'list' and self.get_addon_object())

        if self.should_access_deleted_ratings:
            # For admins or add-on authors replying. When listing, we include
            # deleted ratings but still filter out out replies, because they'll
            # be in the serializer anyway. For other actions, we simply remove
            # any filtering, allowing them to access any rating out of the box
            # with no extra parameter needed.
            if self.action == 'list':
                queryset = Rating.unfiltered.filter(reply_to__isnull=True)
            else:
                queryset = Rating.unfiltered.all()
        elif should_access_only_top_level_ratings:
            # When listing add-on ratings, exclude replies, they'll be
            # included during serialization as children of the relevant
            # ratings instead.
            queryset = Rating.without_replies.all()
        else:
            queryset = Rating.objects.all()

        # Filter out empty ratings if specified.
        # Should the users own empty ratings be filtered back in?
        if 'with_yours' in requested and self.request.user.is_authenticated():
            user_filter = Q(user=self.request.user.pk)
        else:
            user_filter = Q()
        # Apply the filter(s)
        if 'without_empty_body' in requested:
            queryset = queryset.filter(~Q(body=None) | user_filter)

        # The serializer needs reply, version and user. We don't need much
        # for version and user, so we can make joins with select_related(),
        # but for replies additional queries will be made for translations
        # anyway so we're better off using prefetch_related() to make a
        # separate query to fetch them all.
        queryset = queryset.select_related('version', 'user')
        replies_qs = Rating.unfiltered.select_related('user')
        return queryset.prefetch_related(
            Prefetch('reply', queryset=replies_qs))

    @detail_route(
        methods=['post'], permission_classes=reply_permission_classes,
        serializer_class=reply_serializer_class,
        throttle_classes=[RatingReplyThrottle])
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current rating object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)
        if Rating.unfiltered.filter(reply_to=self.rating_object).exists():
            # A reply already exists, just edit it.
            # We set should_access_deleted_ratings so that it works even if
            # the reply has been deleted.
            self.kwargs['pk'] = kwargs['pk'] = self.rating_object.reply.pk
            self.should_access_deleted_ratings = True
            return self.partial_update(*args, **kwargs)
        return self.create(*args, **kwargs)

    @detail_route(methods=['post'], throttle_classes=[])
    def flag(self, request, *args, **kwargs):
        # We load the add-on object from the rating to trigger permission
        # checks.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)

        # Re-use flag view since it's already returning json. We just need to
        # pass it the addon slug (passing it the PK would result in a redirect)
        # and make sure request.POST is set with whatever data was sent to the
        # DRF view.
        request._request.POST = request.data
        request = request._request
        response = flag(request, self.addon_object.slug, kwargs.get('pk'))
        if response.status_code == 200:
            # 202 is a little better than 200: we're accepting the request, but
            # make no promises to act on it :)
            response.status_code = 202
        return response

    def perform_destroy(self, instance):
        instance.delete(user_responsible=self.request.user)
Beispiel #8
0
class ReviewViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = ReviewSerializer
    permission_classes = [
        ByHttpMethod({
            'get': AllowAny,
            'head': AllowAny,
            'options': AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete': CanDeleteReviewPermission,

            # To post a review you just need to be authenticated.
            'post': IsAuthenticated,

            # To edit a review you need to be the author or be an admin.
            'patch': AnyOf(AllowOwner, GroupPermission('Addons', 'Edit')),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [AnyOf(
        GroupPermission('Addons', 'Edit'),
        AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
    )]
    reply_serializer_class = ReviewSerializerReply

    queryset = Review.objects.all()

    def get_addon_object(self):
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(ReviewViewSet, self).get_addon_object(
            permission_classes=[AllowIfReviewedAndListed])

    def check_permissions(self, request):
        if 'addon_pk' in self.kwargs:
            # In addition to the regular permission checks that are made, we
            # need to verify that the add-on exists, is public and listed. Just
            # loading the addon should be enough to do that, since
            # AddonChildMixin implementation calls AddonViewSet.get_object().
            self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(ReviewViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.review_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(ReviewViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            if 'addon_pk' in self.kwargs:
                qs = qs.filter(is_latest=True, addon=self.get_addon_object())
            elif 'account_pk' in self.kwargs:
                qs = qs.filter(user=self.kwargs.get('account_pk'))
            else:
                # Don't allow listing reviews without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user identifier')
        return qs

    def get_paginated_response(self, data):
        response = super(ReviewViewSet, self).get_paginated_response(data)
        show_grouped_ratings = self.request.GET.get('show_grouped_ratings')
        if 'addon_pk' in self.kwargs and show_grouped_ratings:
            response.data['grouped_ratings'] = dict(GroupedRating.get(
                self.addon_object.id))
        return response

    def get_queryset(self):
        requested = self.request.GET.get('filter')

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        self.should_access_deleted_reviews = (
            (requested == 'with_deleted' or self.action != 'list') and
            self.request.user.is_authenticated() and
            acl.action_allowed(self.request, 'Addons', 'Edit'))

        should_access_only_replies = (
            self.action == 'list' and self.kwargs.get('addon_pk'))

        if self.should_access_deleted_reviews:
            # For admins. When listing, we include deleted reviews but still
            # filter out out replies, because they'll be in the serializer
            # anyway. For other actions, we simply remove any filtering,
            # allowing them to access any review out of the box with no
            # extra parameter needed.
            if self.action == 'list':
                self.queryset = Review.unfiltered.filter(reply_to__isnull=True)
            else:
                self.queryset = Review.unfiltered.all()
        elif should_access_only_replies:
            # When listing add-on reviews, exclude replies, they'll be
            # included during serialization as children of the relevant
            # reviews instead.
            self.queryset = Review.without_replies.all()

        qs = super(ReviewViewSet, self).get_queryset()
        if self.action in ('list', 'retrieve'):
            # Also avoid loading addon since we don't need it, we already
            # loaded it for permission checks through the pk specified in the
            # URL. Don't do it for write operations to avoid a bug in django
            # 1.8 and signals (https://github.com/django/django/pull/7274)
            qs = qs.defer('addon')
        # The serializer needs user, reply and version, so use
        # prefetch_related() to avoid extra queries (avoid select_related() as
        # we need crazy joins already).
        return qs.prefetch_related('reply', 'user', 'version')

    @detail_route(
        methods=['post'], permission_classes=reply_permission_classes,
        serializer_class=reply_serializer_class)
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current review object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.review_object = self.get_object()
        return self.create(*args, **kwargs)

    @detail_route(methods=['post'])
    def flag(self, request, *args, **kwargs):
        # Re-use flag view since it's already returning json. We just need to
        # pass it the addon slug (passing it the PK would result in a redirect)
        # and make sure request.POST is set with whatever data was sent to the
        # DRF view.
        addon = self.get_addon_object()
        request._request.POST = request.data
        request = request._request
        response = flag(request, addon.slug, kwargs.get('pk'))
        if response.status_code == 200:
            response.status_code = 202
        return response
Beispiel #9
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # Deletion requires a specific permission check.
            'delete':
            CanDeleteRatingPermission,
            # To post a rating you just need to be authenticated.
            'post':
            IsAuthenticated,
            # To edit a rating you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission(amo.permissions.ADDONS_EDIT)),
            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission(amo.permissions.ADDONS_EDIT),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = RatingSerializerReply
    flag_permission_classes = [AllowNotOwner]
    throttle_classes = (
        RatingUserThrottle,
        RatingIPThrottle,
        RatingEditDeleteIPThrottle,
        RatingEditDeleteUserThrottle,
    )

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = self.request.data.get(
                'addon') or self.request.GET.get('addon')
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_str(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super().get_addon_object(permission_classes=[AllowIfPublic])

    def should_include_flags(self):
        if not hasattr(self, '_should_include_flags'):
            request = self.request
            self._should_include_flags = (
                'show_flags_for' in request.GET
                and not is_gate_active(request, 'del-ratings-flags'))
            if self._should_include_flags:
                # Check the parameter was sent correctly
                try:
                    show_flags_for = serializers.IntegerField(
                    ).to_internal_value(request.GET['show_flags_for'])
                    if show_flags_for != request.user.pk:
                        raise serializers.ValidationError
                except serializers.ValidationError:
                    raise ParseError(
                        'show_flags_for parameter value should be equal to '
                        'the user id of the authenticated user')
        return self._should_include_flags

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super().check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super().get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            score_filter = (self.request.GET.get('score') if is_gate_active(
                self.request, 'ratings-score-filter') else None)
            exclude_ratings = self.request.GET.get('exclude_ratings')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
            if score_filter:
                try:
                    scores = [int(score) for score in score_filter.split(',')]
                except ValueError:
                    raise ParseError(
                        'score parameter should be an integer or a list of '
                        'integers (separated by a comma).')
                qs = qs.filter(rating__in=scores)
            if exclude_ratings:
                try:
                    exclude_ratings = [
                        int(rating) for rating in exclude_ratings.split(',')
                    ]
                except ValueError:
                    raise ParseError('exclude_ratings parameter should be an '
                                     'integer or a list of integers '
                                     '(separated by a comma).')
                qs = qs.exclude(pk__in=exclude_ratings)
        return super().filter_queryset(qs)

    def get_paginated_response(self, data):
        request = self.request
        extra_data = {}
        if grouped_rating := get_grouped_ratings(request,
                                                 self.get_addon_object()):
            extra_data['grouped_ratings'] = grouped_rating
        if 'show_permissions_for' in request.GET and is_gate_active(
                self.request, 'ratings-can_reply'):
            if 'addon' not in request.GET:
                raise ParseError(
                    'show_permissions_for parameter is only valid if the '
                    'addon parameter is also present')
            try:
                show_permissions_for = serializers.IntegerField(
                ).to_internal_value(request.GET['show_permissions_for'])
                if show_permissions_for != request.user.pk:
                    raise serializers.ValidationError
            except serializers.ValidationError:
                raise ParseError(
                    'show_permissions_for parameter value should be equal to '
                    'the user id of the authenticated user')
            extra_data[
                'can_reply'] = self.check_can_reply_permission_for_ratings_list(
                )
        # Call this here so the validation checks on the `show_flags_for` are
        # carried out even when there are no results to serialize.
        self.should_include_flags()
        response = super().get_paginated_response(data)
        if extra_data:
            response.data.update(extra_data)
        return response
Beispiel #10
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # Deletion requires a specific permission check.
            'delete':
            CanDeleteRatingPermission,
            # To post a rating you just need to be authenticated.
            'post':
            IsAuthenticated,
            # To edit a rating you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission(amo.permissions.ADDONS_EDIT)),
            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission(amo.permissions.ADDONS_EDIT),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = RatingSerializerReply
    flag_permission_classes = [AllowNotOwner]
    throttle_classes = (RatingThrottle, )

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = self.request.data.get(
                'addon') or self.request.GET.get('addon')
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_str(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(RatingViewSet,
                     self).get_addon_object(permission_classes=[AllowIfPublic])

    def should_include_flags(self):
        if not hasattr(self, '_should_include_flags'):
            request = self.request
            self._should_include_flags = (
                'show_flags_for' in request.GET
                and not is_gate_active(request, 'del-ratings-flags'))
            if self._should_include_flags:
                # Check the parameter was sent correctly
                try:
                    show_flags_for = serializers.IntegerField(
                    ).to_internal_value(request.GET['show_flags_for'])
                    if show_flags_for != request.user.pk:
                        raise serializers.ValidationError
                except serializers.ValidationError:
                    raise ParseError(
                        'show_flags_for parameter value should be equal to '
                        'the user id of the authenticated user')
        return self._should_include_flags

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(RatingViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(RatingViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            score_filter = (self.request.GET.get('score') if is_gate_active(
                self.request, 'ratings-score-filter') else None)
            exclude_ratings = self.request.GET.get('exclude_ratings')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
            if score_filter:
                try:
                    scores = [int(score) for score in score_filter.split(',')]
                except ValueError:
                    raise ParseError(
                        'score parameter should be an integer or a list of '
                        'integers (separated by a comma).')
                qs = qs.filter(rating__in=scores)
            if exclude_ratings:
                try:
                    exclude_ratings = [
                        int(rating) for rating in exclude_ratings.split(',')
                    ]
                except ValueError:
                    raise ParseError('exclude_ratings parameter should be an '
                                     'integer or a list of integers '
                                     '(separated by a comma).')
                qs = qs.exclude(pk__in=exclude_ratings)
        return super(RatingViewSet, self).filter_queryset(qs)

    def get_paginated_response(self, data):
        request = self.request
        extra_data = {}
        if 'show_grouped_ratings' in request.GET:
            try:
                show_grouped_ratings = serializers.BooleanField(
                ).to_internal_value(request.GET['show_grouped_ratings'])
            except serializers.ValidationError:
                raise ParseError(
                    'show_grouped_ratings parameter should be a boolean')
            if show_grouped_ratings and self.get_addon_object():
                extra_data['grouped_ratings'] = dict(
                    GroupedRating.get(self.addon_object.id))
        if 'show_permissions_for' in request.GET and is_gate_active(
                self.request, 'ratings-can_reply'):
            if 'addon' not in request.GET:
                raise ParseError(
                    'show_permissions_for parameter is only valid if the '
                    'addon parameter is also present')
            try:
                show_permissions_for = serializers.IntegerField(
                ).to_internal_value(request.GET['show_permissions_for'])
                if show_permissions_for != request.user.pk:
                    raise serializers.ValidationError
            except serializers.ValidationError:
                raise ParseError(
                    'show_permissions_for parameter value should be equal to '
                    'the user id of the authenticated user')
            extra_data[
                'can_reply'] = self.check_can_reply_permission_for_ratings_list(
                )
        # Call this here so the validation checks on the `show_flags_for` are
        # carried out even when there are no results to serialize.
        self.should_include_flags()
        response = super(RatingViewSet, self).get_paginated_response(data)
        if extra_data:
            response.data.update(extra_data)
        return response

    def check_can_reply_permission_for_ratings_list(self):
        """Check whether or not the current request contains an user that can
        reply to ratings we're about to return.

        Used to populate the `can_reply` property in ratings list, when an
        addon is passed."""
        # Clone the current viewset, but change permission_classes.
        viewset = self.__class__(**self.__dict__)
        viewset.permission_classes = self.reply_permission_classes

        # Create a fake rating with the addon object attached, to be passed to
        # check_object_permissions().
        dummy_rating = Rating(addon=self.get_addon_object())

        try:
            viewset.check_permissions(self.request)
            viewset.check_object_permissions(self.request, dummy_rating)
            return True
        except (PermissionDenied, NotAuthenticated):
            return False

    def get_queryset(self):
        requested = self.request.GET.get('filter', '').split(',')
        has_addons_edit = acl.action_allowed(self.request,
                                             amo.permissions.ADDONS_EDIT)

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        if not hasattr(self, 'should_access_deleted_ratings'):
            self.should_access_deleted_ratings = (
                ('with_deleted' in requested or self.action != 'list')
                and self.request.user.is_authenticated and has_addons_edit)

        should_access_only_top_level_ratings = (self.action == 'list'
                                                and self.get_addon_object())

        if self.should_access_deleted_ratings:
            # For admins or add-on authors replying. When listing, we include
            # deleted ratings but still filter out out replies, because they'll
            # be in the serializer anyway. For other actions, we simply remove
            # any filtering, allowing them to access any rating out of the box
            # with no extra parameter needed.
            if self.action == 'list':
                queryset = Rating.unfiltered.filter(reply_to__isnull=True)
            else:
                queryset = Rating.unfiltered.all()
        elif should_access_only_top_level_ratings:
            # When listing add-on ratings, exclude replies, they'll be
            # included during serialization as children of the relevant
            # ratings instead.
            queryset = Rating.without_replies.all()
        else:
            queryset = Rating.objects.all()

        # Filter out empty ratings if specified.
        # Should the users own empty ratings be filtered back in?
        if 'with_yours' in requested and self.request.user.is_authenticated:
            user_filter = Q(user=self.request.user.pk)
        else:
            user_filter = Q()
        # Apply the filter(s)
        if 'without_empty_body' in requested:
            queryset = queryset.filter(~Q(body=None) | user_filter)

        # The serializer needs reply, version and user. We don't need much
        # for version and user, so we can make joins with select_related(),
        # but for replies additional queries will be made for translations
        # anyway so we're better off using prefetch_related() to make a
        # separate query to fetch them all.
        queryset = queryset.select_related('version', 'user')
        replies_qs = Rating.unfiltered.select_related('user')
        return queryset.prefetch_related(Prefetch('reply',
                                                  queryset=replies_qs))

    @action(
        detail=True,
        methods=['post'],
        permission_classes=reply_permission_classes,
        serializer_class=reply_serializer_class,
        throttle_classes=[RatingReplyThrottle],
    )
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current rating object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)
        if Rating.unfiltered.filter(reply_to=self.rating_object).exists():
            # A reply already exists, just edit it.
            # We set should_access_deleted_ratings so that it works even if
            # the reply has been deleted.
            self.kwargs['pk'] = kwargs['pk'] = self.rating_object.reply.pk
            self.should_access_deleted_ratings = True
            return self.partial_update(*args, **kwargs)
        return self.create(*args, **kwargs)

    @action(
        detail=True,
        methods=['post'],
        permission_classes=flag_permission_classes,
        throttle_classes=[],
    )
    def flag(self, request, *args, **kwargs):
        # We load the add-on object from the rating to trigger permission
        # checks.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)

        try:
            flag_instance = RatingFlag.objects.get(rating=self.rating_object,
                                                   user=self.request.user)
        except RatingFlag.DoesNotExist:
            flag_instance = None
        if flag_instance is None:
            serializer = RatingFlagSerializer(
                data=request.data, context=self.get_serializer_context())
        else:
            serializer = RatingFlagSerializer(
                flag_instance,
                data=request.data,
                partial=False,
                context=self.get_serializer_context(),
            )
        serializer.is_valid(raise_exception=True)
        serializer.save()
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data,
                        status=HTTP_202_ACCEPTED,
                        headers=headers)

    def perform_destroy(self, instance):
        instance.delete(user_responsible=self.request.user)
Beispiel #11
0
class ReviewViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = ReviewSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete':
            CanDeleteReviewPermission,

            # To post a review you just need to be authenticated.
            'post':
            IsAuthenticated,

            # To edit a review you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission('Addons', 'Edit')),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission('Addons', 'Edit'),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = ReviewSerializerReply

    queryset = Review.objects.all()

    def get_addon_object(self):
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(ReviewViewSet, self).get_addon_object(
            permission_classes=[AllowIfReviewedAndListed])

    def check_permissions(self, request):
        if 'addon_pk' in self.kwargs:
            # In addition to the regular permission checks that are made, we
            # need to verify that the add-on exists, is public and listed. Just
            # loading the addon should be enough to do that, since
            # AddonChildMixin implementation calls AddonViewSet.get_object().
            self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(ReviewViewSet, self).check_permissions(request)

    def filter_queryset(self, qs):
        if self.action == 'list':
            if 'addon_pk' in self.kwargs:
                qs = qs.filter(addon=self.get_addon_object())
            elif 'account_pk' in self.kwargs:
                qs = qs.filter(user=self.kwargs.get('account_pk'))
            else:
                # Don't allow listing reviews without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user identifier')
        return qs

    def get_queryset(self):
        if self.action == 'list':
            if self.kwargs.get('addon_pk'):
                # When listing add-on reviews, exclude replies, they'll be
                # included during serialization as children of the relevant
                # reviews instead.
                self.queryset = Review.without_replies.all()

        qs = super(ReviewViewSet, self).get_queryset()
        # The serializer needs user, reply and version, so use
        # prefetch_related() to avoid extra queries (avoid select_related() as
        # we need crazy joins already). Also avoid loading addon since we don't
        # need it, we already loaded it for permission checks through the pk
        # specified in the URL.
        return qs.defer('addon').prefetch_related('reply', 'user', 'version')

    @detail_route(methods=['post'],
                  permission_classes=reply_permission_classes,
                  serializer_class=reply_serializer_class)
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current review object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.review_object = self.get_object()
        return self.create(*args, **kwargs)