Allow app owners and admin to see ratings from non-public apps (bug 943093)

This commit is contained in:
Mathieu Pillard 2013-11-29 16:14:19 +01:00 коммит произвёл Mathieu Pillard
Родитель 35b1435413
Коммит 5cc7d38808
3 изменённых файлов: 86 добавлений и 36 удалений

Просмотреть файл

@ -8,7 +8,7 @@ from reviews.models import Review, ReviewFlag
from mkt.account.serializers import AccountSerializer from mkt.account.serializers import AccountSerializer
from mkt.api.fields import SlugOrPrimaryKeyRelatedField, SplitField from mkt.api.fields import SlugOrPrimaryKeyRelatedField, SplitField
from mkt.api.exceptions import Conflict from mkt.api.exceptions import Conflict
from mkt.regions import get_region, REGIONS_DICT from mkt.regions import REGIONS_DICT, get_region
from mkt.versions.api import SimpleVersionSerializer from mkt.versions.api import SimpleVersionSerializer
from mkt.webapps.models import Webapp from mkt.webapps.models import Webapp
@ -60,25 +60,16 @@ class RatingSerializer(serializers.ModelSerializer):
return (not self.get_is_author(obj) and return (not self.get_is_author(obj) and
obj.reviewflag_set.filter(user=self.request.amo_user).exists()) obj.reviewflag_set.filter(user=self.request.amo_user).exists())
@classmethod
def get_app_from_value(cls, value):
try:
app = Webapp.objects.valid().get(id=value)
except (Webapp.DoesNotExist, ValueError):
try:
app = Webapp.objects.valid().get(app_slug=value)
except Webapp.DoesNotExist:
raise serializers.ValidationError('Invalid app')
if not app.listed_in(region=REGIONS_DICT[get_region()]):
raise serializers.ValidationError(
'App not available in this region')
return app
def validate(self, attrs): def validate(self, attrs):
attrs['user'] = self.request.amo_user attrs['user'] = self.request.amo_user
attrs['ip_address'] = self.request.META.get('REMOTE_ADDR', '') attrs['ip_address'] = self.request.META.get('REMOTE_ADDR', '')
if not getattr(self, 'object'): if not getattr(self, 'object'):
# If we are creating a rating, then we need to do various checks on
# the app. Because these checks need the version as well, we have
# to do them here and not in validate_app().
# If the app is packaged, add in the current version.
if attrs['addon'].is_packaged: if attrs['addon'].is_packaged:
attrs['version'] = attrs['addon'].current_version attrs['version'] = attrs['addon'].current_version
@ -88,11 +79,14 @@ class RatingSerializer(serializers.ModelSerializer):
qs = self.context['view'].queryset.filter(addon=app, user=amo_user) qs = self.context['view'].queryset.filter(addon=app, user=amo_user)
if app.is_packaged: if app.is_packaged:
qs = qs.filter(version=attrs['version']) qs = qs.filter(version=attrs['version'])
if qs.exists(): if qs.exists():
raise Conflict('You have already reviewed this app.') raise Conflict('You have already reviewed this app.')
# Return 403 if the user is attempting to review their own app: # Return 403 is the app is not public.
if not app.is_public():
raise PermissionDenied('The app requested is not public.')
# Return 403 if the user is attempting to review their own app.
if app.has_author(amo_user): if app.has_author(amo_user):
raise PermissionDenied('You may not review your own app.') raise PermissionDenied('You may not review your own app.')
@ -100,13 +94,18 @@ class RatingSerializer(serializers.ModelSerializer):
if app.is_premium() and not app.is_purchased(amo_user): if app.is_premium() and not app.is_purchased(amo_user):
raise PermissionDenied("You may not review paid apps you " raise PermissionDenied("You may not review paid apps you "
"haven't purchased.") "haven't purchased.")
# Return 403 if the app is not available in the current region.
current_region = get_region()
if not app.listed_in(region=REGIONS_DICT[current_region]):
raise PermissionDenied('App not available in region "%s".' %
current_region)
return attrs return attrs
def validate_app(self, attrs, source): def validate_app(self, attrs, source):
if not getattr(self, 'object'): # Don't allow users to change the app on an existing rating.
app = attrs[source] if getattr(self, 'object'):
attrs[source] = RatingSerializer.get_app_from_value(app.pk)
else:
attrs[source] = self.object.addon attrs[source] = self.object.addon
return attrs return attrs

Просмотреть файл

@ -161,21 +161,59 @@ class TestRatingResource(RestOAuth, amo.tests.AMOPaths):
eq_(data['info']['slug'], self.app.app_slug) eq_(data['info']['slug'], self.app.app_slug)
eq_(data['info']['current_version'], self.app.current_version.version) eq_(data['info']['current_version'], self.app.current_version.version)
def test_filter_by_invalid_app_slug(self): def test_filter_by_invalid_app(self):
Review.objects.create(addon=self.app, user=self.user, body='yes') Review.objects.create(addon=self.app, user=self.user, body='yes')
self._get_filter(app='wrongslug', expected_status=400) self._get_filter(app='wrongslug', expected_status=404)
self._get_filter(app=2465478, expected_status=404)
def test_filter_by_nonpublic_app(self): @patch('mkt.ratings.views.get_region')
def test_filter_by_nonpublic_app(self, get_region_mock):
Review.objects.create(addon=self.app, user=self.user, body='yes')
self.app.update(status=amo.STATUS_PENDING) self.app.update(status=amo.STATUS_PENDING)
self._get_filter(app=self.app.app_slug, expected_status=400) get_region_mock.return_value = 'us'
res, data = self._get_filter(app=self.app.app_slug, expected_status=403)
eq_(data['detail'], 'The app requested is not public or not available '
'in region "us".')
@patch('mkt.ratings.serializers.get_region') def test_filter_by_nonpublic_app_admin(self):
Review.objects.create(addon=self.app, user=self.user, body='yes')
self.grant_permission(self.user, 'Addons:Edit')
self.app.update(status=amo.STATUS_PENDING)
self._get_filter(app=self.app.app_slug)
def test_filter_by_nonpublic_app_owner(self):
Review.objects.create(addon=self.app, user=self.user, body='yes')
AddonUser.objects.create(user=self.user, addon=self.app)
self.app.update(status=amo.STATUS_PENDING)
self._get_filter(app=self.app.app_slug)
@patch('mkt.ratings.views.get_region')
def test_filter_by_app_excluded_in_region(self, get_region_mock): def test_filter_by_app_excluded_in_region(self, get_region_mock):
Review.objects.create(addon=self.app, user=self.user, body='yes')
AddonExcludedRegion.objects.create(addon=self.app, AddonExcludedRegion.objects.create(addon=self.app,
region=mkt.regions.BR.id) region=mkt.regions.BR.id)
get_region_mock.return_value = 'br' get_region_mock.return_value = 'br'
r, data = self._get_filter(app=self.app.app_slug, expected_status=400) res, data = self._get_filter(app=self.app.app_slug, expected_status=403)
eq_(data['detail'], 'App not available in this region') eq_(data['detail'], 'The app requested is not public or not available '
'in region "br".')
@patch('mkt.ratings.views.get_region')
def test_filter_by_app_excluded_in_region_admin(self, get_region_mock):
Review.objects.create(addon=self.app, user=self.user, body='yes')
self.grant_permission(self.user, 'Addons:Edit')
AddonExcludedRegion.objects.create(addon=self.app,
region=mkt.regions.BR.id)
get_region_mock.return_value = 'br'
self._get_filter(app=self.app.app_slug)
@patch('mkt.ratings.views.get_region')
def test_filter_by_app_excluded_in_region_owner(self, get_region_mock):
Review.objects.create(addon=self.app, user=self.user, body='yes')
AddonUser.objects.create(user=self.user, addon=self.app)
AddonExcludedRegion.objects.create(addon=self.app,
region=mkt.regions.BR.id)
get_region_mock.return_value = 'br'
self._get_filter(app=self.app.app_slug)
def test_anonymous_get_list_without_app(self): def test_anonymous_get_list_without_app(self):
res, data = self._get_url(self.list_url, client=self.anon) res, data = self._get_url(self.list_url, client=self.anon)
@ -302,12 +340,12 @@ class TestRatingResource(RestOAuth, amo.tests.AMOPaths):
region=mkt.regions.BR.id) region=mkt.regions.BR.id)
get_region_mock.return_value = 'br' get_region_mock.return_value = 'br'
res, data = self._create() res, data = self._create()
eq_(400, res.status_code) eq_(403, res.status_code)
def test_create_for_nonpublic(self): def test_create_for_nonpublic(self):
self.app.update(status=amo.STATUS_PENDING) self.app.update(status=amo.STATUS_PENDING)
res, data = self._create() res, data = self._create()
eq_(400, res.status_code) eq_(403, res.status_code)
def test_create_duplicate_rating(self): def test_create_duplicate_rating(self):
self._create() self._create()

Просмотреть файл

@ -1,15 +1,16 @@
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import Http404
import commonware.log import commonware.log
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import (MethodNotAllowed, NotAuthenticated, from rest_framework.exceptions import (MethodNotAllowed, NotAuthenticated,
ParseError) PermissionDenied)
from rest_framework.mixins import CreateModelMixin from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
import amo import amo
from access.acl import check_addon_ownership
from lib.metrics import record_action from lib.metrics import record_action
from reviews.models import Review, ReviewFlag from reviews.models import Review, ReviewFlag
@ -20,6 +21,8 @@ from mkt.api.authorization import (AnyOf, AllowOwner, AllowRelatedAppOwner,
ByHttpMethod, GroupPermission) ByHttpMethod, GroupPermission)
from mkt.api.base import CORSMixin from mkt.api.base import CORSMixin
from mkt.ratings.serializers import RatingFlagSerializer, RatingSerializer from mkt.ratings.serializers import RatingFlagSerializer, RatingSerializer
from mkt.regions import REGIONS_DICT, get_region
from mkt.webapps.models import Webapp
log = commonware.log.getLogger('z.api') log = commonware.log.getLogger('z.api')
@ -110,9 +113,19 @@ class RatingViewSet(CORSMixin, ModelViewSet):
def get_app(self, ident): def get_app(self, ident):
try: try:
return self.serializer_class.get_app_from_value(ident) app = Webapp.objects.by_identifier(ident)
except ValidationError as e: except Webapp.DoesNotExist:
raise ParseError(detail=e.messages[0]) raise Http404
current_region = get_region()
if ((not app.is_public()
or not app.listed_in(region=REGIONS_DICT[current_region]))
and not check_addon_ownership(self.request, app)):
# App owners and admin can see the app even if it's not public
# or not available in the current region. Regular users or
# anonymous users can't.
raise PermissionDenied('The app requested is not public or not '
'available in region "%s".' % current_region)
return app
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
response = super(RatingViewSet, self).list(request, *args, **kwargs) response = super(RatingViewSet, self).list(request, *args, **kwargs)
@ -143,7 +156,7 @@ class RatingViewSet(CORSMixin, ModelViewSet):
(obj.pk, self.request.amo_user.id)) (obj.pk, self.request.amo_user.id))
def partial_update(self, *args, **kwargs): def partial_update(self, *args, **kwargs):
# We don't need/want PATCH. # We don't need/want PATCH for now.
raise MethodNotAllowed('PATCH is not supported for this endpoint.') raise MethodNotAllowed('PATCH is not supported for this endpoint.')
def get_extra_data(self, app, amo_user): def get_extra_data(self, app, amo_user):