This commit is contained in:
Andrew Williamson 2017-07-25 15:29:44 +01:00 коммит произвёл GitHub
Родитель 4f85865cc0
Коммит 04e09fe6f4
14 изменённых файлов: 529 добавлений и 14 удалений

59
docs/topics/api/abuse.rst Normal file
Просмотреть файл

@ -0,0 +1,59 @@
=============
Abuse Reports
=============
The following API endpoint covers abuse reporting
---------------------------------
Submitting an add-on abuse report
---------------------------------
.. _`addonabusereport-create`:
The following API endpoint allows an abuse report to be submitted for an Add-on,
either listed on https://addons.mozilla.org or not.
Authentication is not required, but is recommended so reports can be responded
to if nessecary.
.. http:post:: /api/v3/abuse/report/addon/
.. _addonabusereport-create-request:
:<json string addon: The id, slug, or guid of the add-on to report for abuse (required).
:<json string message: The body/content of the abuse report (required).
:>json object|null reporter: The user who submitted the report, if authenticated.
:>json int reporter.id: The id of the user who submitted the report.
:>json string reporter.name: The name of the user who submitted the report.
:>json string reporter.url: The link to the profile page for of the user who submitted the report.
:>json object addon: The add-on reported for abuse.
:>json string addon.guid: The add-on `extension identifier <https://developer.mozilla.org/en-US/Add-ons/Install_Manifests#id>`_.
:>json int|null addon.id: The add-on id on AMO. If the guid submitted didn't match a known add-on on AMO, then null.
:>json string|null addon.slug: The add-on slug. If the guid submitted didn't match a known add-on on AMO, then null.
:>json string message: The body/content of the abuse report.
------------------------------
Submitting a user abuse report
------------------------------
.. _`userabusereport-create`:
The following API endpoint allows an abuse report to be submitted for a user account
on https://addons.mozilla.org. Authentication is not required, but is recommended
so reports can be responded to if nessecary.
.. http:post:: /api/v3/abuse/report/user/
.. _userabusereport-create-request:
:<json string user: The id or username of the user to report for abuse (required).
:<json string message: The body/content of the abuse report (required).
:>json object|null reporter: The user who submitted the report, if authenticated.
:>json int reporter.id: The id of the user who submitted the report.
:>json string reporter.name: The name of the user who submitted the report.
:>json string reporter.url: The link to the profile page for of the user who submitted the report.
:>json object user: The user reported for abuse.
:>json int user.id: The id of the user reported.
:>json string user.name: The name of the user reported.
:>json string user.url: The link to the profile page for of the user reported.
:>json string message: The body/content of the abuse report.

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

@ -32,6 +32,7 @@ using the API.
overview
auth
auth_internal
abuse
accounts
activity
addons

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

@ -1,4 +1,5 @@
from django.contrib import admin
from django.db.models import Q
from .models import AbuseReport
@ -32,14 +33,15 @@ class AbuseReportTypeFilter(admin.SimpleListFilter):
if self.value() == 'user':
return queryset.filter(user__isnull=False)
elif self.value() == 'addon':
return queryset.filter(addon__isnull=False)
return queryset.filter(Q(addon__isnull=False) |
Q(guid__isnull=False))
return queryset
class AbuseReportAdmin(admin.ModelAdmin):
raw_id_fields = ('addon', 'user', 'reporter')
readonly_fields = ('ip_address', 'message', 'created', 'addon', 'user',
'reporter')
'guid', 'reporter')
list_display = ('reporter', 'ip_address', 'type', 'target', 'message',
'created')
list_filter = (AbuseReportTypeFilter, )

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

@ -14,9 +14,13 @@ class AbuseReport(ModelBase):
reporter = models.ForeignKey(UserProfile, null=True,
blank=True, related_name='abuse_reported')
ip_address = models.CharField(max_length=255, default='0.0.0.0')
# An abuse report can be for an addon or a user. Only one of these should
# be null.
# An abuse report can be for an addon or a user.
# If user is non-null then both addon and guid should be null.
# If user is null then addon should be non-null if guid was in our DB,
# otherwise addon will be null also.
# If both addon and user is null guid should be set.
addon = models.ForeignKey(Addon, null=True, related_name='abuse_reports')
guid = models.CharField(max_length=255, null=True)
user = models.ForeignKey(UserProfile, null=True,
related_name='abuse_reports')
message = models.TextField()
@ -30,11 +34,12 @@ class AbuseReport(ModelBase):
else:
user_name = 'An anonymous coward'
msg = u'%s reported abuse for %s (%s%s).\n\n%s' % (
user_name, self.target.name, settings.SITE_URL,
self.target.get_url_path(), self.message)
send_mail(unicode(self), msg,
recipient_list=(settings.ABUSE_EMAIL,))
target_url = ('%s%s' % (settings.SITE_URL, self.target.get_url_path())
if self.target else 'GUID not in database')
name = self.target.name if self.target else self.guid
msg = u'%s reported abuse for %s (%s).\n\n%s' % (
user_name, name, target_url, self.message)
send_mail(unicode(self), msg, recipient_list=(settings.ABUSE_EMAIL,))
@property
def target(self):
@ -44,14 +49,16 @@ class AbuseReport(ModelBase):
def type(self):
with no_translation():
type_ = (ugettext(amo.ADDON_TYPE[self.addon.type])
if self.addon else 'User')
if self.addon else 'User' if self.user else 'Addon')
return type_
def __unicode__(self):
return u'[%s] Abuse Report for %s' % (self.type, self.target.name)
name = self.target.name if self.target else self.guid
return u'[%s] Abuse Report for %s' % (self.type, name)
def send_abuse_report(request, obj, message):
# Only used by legacy frontend
report = AbuseReport(ip_address=request.META.get('REMOTE_ADDR'),
message=message)
if request.user.is_authenticated():

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

@ -0,0 +1,32 @@
from rest_framework import serializers
from olympia.abuse.models import AbuseReport
from olympia.users.serializers import BaseUserSerializer
class AddonAbuseReportSerializer(serializers.ModelSerializer):
addon = serializers.SerializerMethodField()
reporter = BaseUserSerializer(read_only=True)
class Meta:
model = AbuseReport
fields = ('reporter', 'addon', 'message')
def get_addon(self, obj):
addon = obj.addon
if not addon and not obj.guid:
return None
return {
'guid': addon.guid if addon else obj.guid,
'id': addon.id if addon else None,
'slug': addon.slug if addon else None,
}
class UserAbuseReportSerializer(serializers.ModelSerializer):
reporter = BaseUserSerializer(read_only=True)
user = BaseUserSerializer()
class Meta:
model = AbuseReport
fields = ('reporter', 'user', 'message')

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

@ -21,6 +21,8 @@ class TestAbuse(TestCase):
AbuseReport.objects.create(
addon=addon, ip_address='1.2.3.4', reporter=user_factory(),
message='Bar')
# This is a report for an addon not in the database
AbuseReport.objects.create(guid='@guid', message='Foo')
AbuseReport.objects.create(user=user_factory(), message='Eheheheh')
url = reverse('admin:abuse_abusereport_changelist')
@ -29,12 +31,12 @@ class TestAbuse(TestCase):
response = self.client.get(url)
assert response.status_code == 200
doc = pq(response.content)
assert doc('#result_list tbody tr').length == 3
assert doc('#result_list tbody tr').length == 4
response = self.client.get(url, {'type': 'addon'})
assert response.status_code == 200
doc = pq(response.content)
assert doc('#result_list tbody tr').length == 2
assert doc('#result_list tbody tr').length == 3
response = self.client.get(url, {'type': 'user'})
assert response.status_code == 200

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

@ -18,6 +18,7 @@ class TestAbuse(TestCase):
assert (
mail.outbox[0].subject ==
u'[User] Abuse Report for regularuser التطب')
assert 'user/regularuser' in mail.outbox[0].body
assert mail.outbox[0].to == [settings.ABUSE_EMAIL]
@ -30,6 +31,7 @@ class TestAbuse(TestCase):
assert (
mail.outbox[0].subject ==
u'[Extension] Abuse Report for Delicious Bookmarks')
assert 'addon/a3615' in mail.outbox[0].body
def test_addon_fr(self):
with self.activate(locale='fr'):
@ -41,3 +43,14 @@ class TestAbuse(TestCase):
assert (
mail.outbox[0].subject ==
u'[Extension] Abuse Report for Delicious Bookmarks')
def test_guid(self):
report = AbuseReport(guid='foo@bar.org')
report.send()
assert (
unicode(report) ==
u'[Addon] Abuse Report for foo@bar.org')
assert (
mail.outbox[0].subject ==
u'[Addon] Abuse Report for foo@bar.org')
assert 'GUID not in database' in mail.outbox[0].body

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

@ -0,0 +1,46 @@
from olympia.abuse.models import AbuseReport
from olympia.abuse.serializers import (
AddonAbuseReportSerializer, UserAbuseReportSerializer)
from olympia.amo.tests import (
addon_factory, BaseTestCase, user_factory)
from olympia.users.serializers import BaseUserSerializer
class TestAddonAbuseReportSerializer(BaseTestCase):
def serialize(self, report, **extra_context):
return AddonAbuseReportSerializer(report, context=extra_context).data
def test_addon_report(self):
addon = addon_factory(guid='@guid')
report = AbuseReport(addon=addon, message='bad stuff')
serial = self.serialize(report)
assert serial == {'reporter': None,
'addon': {'guid': addon.guid,
'id': addon.id,
'slug': addon.slug},
'message': 'bad stuff'}
def test_guid_report(self):
report = AbuseReport(guid='@guid', message='bad stuff')
serial = self.serialize(report)
assert serial == {'reporter': None,
'addon': {'guid': '@guid',
'id': None,
'slug': None},
'message': 'bad stuff'}
class TestUserAbuseReportSerializer(BaseTestCase):
def serialize(self, report, **extra_context):
return UserAbuseReportSerializer(report, context=extra_context).data
def test_user_report(self):
user = user_factory()
report = AbuseReport(user=user, message='bad stuff')
serial = self.serialize(report)
user_serial = BaseUserSerializer(user).data
assert serial == {'reporter': None,
'user': user_serial,
'message': 'bad stuff'}

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

@ -0,0 +1,217 @@
import json
from django.core import mail
from django.core.urlresolvers import reverse
from olympia import amo
from olympia.abuse.models import AbuseReport
from olympia.amo.tests import (
addon_factory, APITestClient, TestCase, user_factory)
class AddonAbuseReviewSetTestBase(object):
client_class = APITestClient
def setUp(self):
self.url = reverse('abusereportaddon-list')
def check_reporter(self, report):
raise NotImplementedError
def check_report(self, report, text):
assert unicode(report) == text
assert report.ip_address == '123.45.67.89'
assert mail.outbox[0].subject == text
self.check_reporter(report)
def test_report_addon_by_id(self):
addon = addon_factory()
response = self.client.post(
self.url,
data={'addon': unicode(addon.id), 'message': 'abuse!'},
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201
assert AbuseReport.objects.filter(addon_id=addon.id).exists()
report = AbuseReport.objects.get(addon_id=addon.id)
self.check_report(report,
u'[Extension] Abuse Report for %s' % addon.name)
def test_report_addon_by_slug(self):
addon = addon_factory()
response = self.client.post(
self.url,
data={'addon': addon.slug, 'message': 'abuse!'},
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201
assert AbuseReport.objects.filter(addon_id=addon.id).exists()
report = AbuseReport.objects.get(addon_id=addon.id)
self.check_report(report,
u'[Extension] Abuse Report for %s' % addon.name)
def test_report_addon_by_guid(self):
addon = addon_factory(guid='@badman')
response = self.client.post(
self.url,
data={'addon': addon.guid, 'message': 'abuse!'},
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201
assert AbuseReport.objects.filter(addon_id=addon.id).exists()
report = AbuseReport.objects.get(addon_id=addon.id)
self.check_report(report,
u'[Extension] Abuse Report for %s' % addon.name)
def test_report_addon_guid_not_on_amo(self):
guid = '@mysteryman'
response = self.client.post(
self.url,
data={'addon': guid, 'message': 'abuse!'},
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201
assert AbuseReport.objects.filter(guid=guid).exists()
report = AbuseReport.objects.get(guid=guid)
self.check_report(report,
u'[Addon] Abuse Report for %s' % guid)
def test_report_addon_invalid_identifier(self):
response = self.client.post(
self.url,
data={'addon': 'randomnotguid', 'message': 'abuse!'})
assert response.status_code == 404
def test_addon_not_public(self):
addon = addon_factory(status=amo.STATUS_NULL)
response = self.client.post(
self.url,
data={'addon': unicode(addon.id), 'message': 'abuse!'},
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201
assert AbuseReport.objects.filter(addon_id=addon.id).exists()
report = AbuseReport.objects.get(addon_id=addon.id)
self.check_report(report,
u'[Extension] Abuse Report for %s' % addon.name)
def test_no_addon_fails(self):
response = self.client.post(
self.url,
data={'message': 'abuse!'})
assert response.status_code == 400
assert json.loads(response.content) == {
'detail': 'Need an addon parameter'}
def test_message_required(self):
addon = addon_factory()
response = self.client.post(
self.url,
data={'addon': unicode(addon.id),
'message': ''})
assert response.status_code == 400
assert json.loads(response.content) == {
'detail': 'Abuse reports need a message'}
response = self.client.post(
self.url,
data={'addon': unicode(addon.id)})
assert response.status_code == 400
assert json.loads(response.content) == {
'detail': 'Abuse reports need a message'}
class TestAddonAbuseReviewSetLoggedOut(AddonAbuseReviewSetTestBase, TestCase):
def check_reporter(self, report):
assert not report.reporter
class TestAddonAbuseReviewSetLoggedIn(AddonAbuseReviewSetTestBase, TestCase):
def setUp(self):
super(TestAddonAbuseReviewSetLoggedIn, self).setUp()
self.user = user_factory()
self.client.login_api(self.user)
def check_reporter(self, report):
assert report.reporter == self.user
class UserAbuseReviewSetTestBase(object):
client_class = APITestClient
def setUp(self):
self.url = reverse('abusereportuser-list')
def check_reporter(self, report):
raise NotImplementedError
def check_report(self, report, text):
assert unicode(report) == text
assert report.ip_address == '123.45.67.89'
assert mail.outbox[0].subject == text
self.check_reporter(report)
def test_report_user_id(self):
user = user_factory()
response = self.client.post(
self.url,
data={'user': unicode(user.id), 'message': 'abuse!'},
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201
assert AbuseReport.objects.filter(user_id=user.id).exists()
report = AbuseReport.objects.get(user_id=user.id)
self.check_report(report,
u'[User] Abuse Report for %s' % user.name)
def test_report_user_username(self):
user = user_factory()
response = self.client.post(
self.url,
data={'user': unicode(user.username), 'message': 'abuse!'},
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201
assert AbuseReport.objects.filter(user_id=user.id).exists()
report = AbuseReport.objects.get(user_id=user.id)
self.check_report(report,
u'[User] Abuse Report for %s' % user.name)
def test_no_user_fails(self):
response = self.client.post(
self.url,
data={'message': 'abuse!'})
assert response.status_code == 400
assert json.loads(response.content) == {
'detail': 'Need a user parameter'}
def test_message_required(self):
user = user_factory()
response = self.client.post(
self.url,
data={'user': unicode(user.username), 'message': ''})
assert response.status_code == 400
assert json.loads(response.content) == {
'detail': 'Abuse reports need a message'}
response = self.client.post(
self.url,
data={'user': unicode(user.username)})
assert response.status_code == 400
assert json.loads(response.content) == {
'detail': 'Abuse reports need a message'}
class TestUserAbuseReviewSetLoggedOut(UserAbuseReviewSetTestBase, TestCase):
def check_reporter(self, report):
assert not report.reporter
class TestUserAbuseReviewSetLoggedIn(UserAbuseReviewSetTestBase, TestCase):
def setUp(self):
super(TestUserAbuseReviewSetLoggedIn, self).setUp()
self.user = user_factory()
self.client.login_api(self.user)
def check_reporter(self, report):
assert report.reporter == self.user

16
src/olympia/abuse/urls.py Normal file
Просмотреть файл

@ -0,0 +1,16 @@
from django.conf.urls import include, url
from rest_framework.routers import SimpleRouter
from views import AddonAbuseViewSet, UserAbuseViewSet
reporting = SimpleRouter()
reporting.register(r'addon', AddonAbuseViewSet,
base_name='abusereportaddon')
reporting.register(r'user', UserAbuseViewSet,
base_name='abusereportuser')
urlpatterns = [
url(r'report/', include(reporting.urls)),
]

117
src/olympia/abuse/views.py Normal file
Просмотреть файл

@ -0,0 +1,117 @@
from django.http import Http404
from rest_framework import status
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from olympia.accounts.views import AccountViewSet
from olympia.abuse.models import AbuseReport
from olympia.abuse.serializers import (
AddonAbuseReportSerializer, UserAbuseReportSerializer)
from olympia.addons.views import AddonViewSet
class AddonAbuseViewSet(CreateModelMixin, GenericViewSet):
permission_classes = []
serializer_class = AddonAbuseReportSerializer
def get_addon_viewset(self):
if hasattr(self, 'addon_viewset'):
return self.addon_viewset
if 'addon_pk' not in self.kwargs:
self.kwargs['addon_pk'] = (
self.request.data.get('addon') or
self.request.GET.get('addon'))
self.addon_viewset = AddonViewSet(
request=self.request, permission_classes=[],
kwargs={'pk': self.kwargs['addon_pk']})
return self.addon_viewset
def get_addon_object(self):
if hasattr(self, 'addon_object'):
return self.addon_object
self.addon_object = self.get_addon_viewset().get_object()
return self.addon_object
def get_guid(self):
# See if the addon input is guid-like, if so set guid.
if self.get_addon_viewset().get_lookup_field(
self.kwargs['addon_pk']) == 'guid':
guid = self.kwargs['addon_pk']
try:
# But see if it's also in our database.
self.get_addon_object()
except Http404:
# If it isn't, that's okay, we have a guid. Setting
# addon_object=None here means get_addon_object won't raise 404
self.addon_object = None
return guid
return None
def create(self, request, *args, **kwargs):
addon_id = self.request.data.get('addon')
if not addon_id:
raise ParseError('Need an addon parameter')
message = self.request.data.get('message')
if not message:
raise ParseError('Abuse reports need a message')
abuse_kwargs = {
'ip_address': request.META.get('REMOTE_ADDR'),
'message': message,
# get_guid() must be called first or addons not in our DB will 404.
'guid': self.get_guid(),
'addon': self.get_addon_object()}
if request.user.is_authenticated():
abuse_kwargs['reporter'] = request.user
report = AbuseReport.objects.create(**abuse_kwargs)
report.send()
serializer = self.get_serializer(report)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class UserAbuseViewSet(CreateModelMixin, GenericViewSet):
permission_classes = []
serializer_class = UserAbuseReportSerializer
def get_user_object(self, user_id):
if hasattr(self, 'user_object'):
return self.user_object
if 'user_pk' not in self.kwargs:
self.kwargs['user_pk'] = (
self.request.data.get('user') or
self.request.GET.get('user'))
return AccountViewSet(
request=self.request, permission_classes=[],
kwargs={'pk': self.kwargs['user_pk']}).get_object()
def create(self, request, *args, **kwargs):
user_id = self.request.data.get('user')
if not user_id:
raise ParseError('Need a user parameter')
message = self.request.data.get('message')
if not message:
raise ParseError('Abuse reports need a message')
abuse_kwargs = {
'ip_address': request.META.get('REMOTE_ADDR'),
'message': message,
'user': self.get_user_object(user_id)}
if request.user.is_authenticated():
abuse_kwargs['reporter'] = request.user
report = AbuseReport.objects.create(**abuse_kwargs)
report.send()
serializer = self.get_serializer(report)
return Response(serializer.data, status=status.HTTP_201_CREATED)

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

@ -626,7 +626,7 @@ class AddonViewSet(RetrieveModelMixin, GenericViewSet):
class AddonChildMixin(object):
"""Mixin containing method to retrive the parent add-on object."""
"""Mixin containing method to retrieve the parent add-on object."""
def get_addon_object(self, permission_classes=None, lookup='addon_pk'):
"""Return the parent Addon object using the URL parameter passed

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

@ -2,6 +2,7 @@ from django.conf.urls import include, url
urlpatterns = [
url(r'^v3/abuse/', include('olympia.abuse.urls')),
url(r'^v3/accounts/', include('olympia.accounts.urls')),
url(r'^v3/addons/', include('olympia.addons.api_urls')),
url(r'^v3/', include('olympia.discovery.api_urls')),

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

@ -0,0 +1,2 @@
ALTER TABLE `abuse_reports`
ADD COLUMN `guid` CHAR(255) NULL;