Augment the Abuse Report API with new fields

Those new fields are meant to be used for add-on reporting from
within Firefox itself.
This commit is contained in:
Mathieu Pillard 2019-02-18 18:35:13 +01:00
Родитель 66af052c1a
Коммит a768edd15b
7 изменённых файлов: 269 добавлений и 8 удалений

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

@ -19,7 +19,7 @@ Submitting an add-on abuse report
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.
to if necessary.
.. http:post:: /api/v4/abuse/report/addon/
@ -27,6 +27,20 @@ to if nessecary.
:<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 string|null addon_install_entry_point: The add-on install entry point. Accepted values: TBD.
:<json string|null addon_install_method: The add-on install method. Accepted values: TBD.
:<json string|null addon_install_origin: The add-on install origin.
:<json string|null addon_name: The add-on name in the locale used by the client.
:<json string|null addon_signature: The add-on signature state. Accepted values: TBD.
:<json string|null addon_summary: The add-on summary in the locale used by the client.
:<json string|null addon_version: The add-on version string.
:<json string|null application: The application used by the client. Can be either ``firefox`` or ``android``.
:<json string|null application_locale: The locale used by the client for the application.
:<json string|null client_id: The client's hashed telemetry ID.
:<json string|null install_date: The add-on install date.
:<json string|null operating_system: The client's operating system.
:<json string|null operating_system_version: The client's operating system version.
:<json string|null reason: The reason for the report. Can be ``malware``, ``spam_or_advertising``, ``search_takeover``, ``new_tab_takeover``, `breaks_website``, `offensive``, `does_not_match_description`` or ``does_not_work``.
:>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.
@ -37,6 +51,20 @@ to if nessecary.
:>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.
:>json string|null addon_install_entry_point: The add-on install entry point.
:>json string|null addon_install_method: The add-on install method.
:>json string|null addon_install_origin: The add-on install origin.
:>json string|null addon_name: The add-on name in the locale used by the client.
:>json string|null addon_signature: The add-on signature state.
:>json string|null addon_summary: The add-on summary in the locale used by the client.
:>json string|null addon_version: The add-on version string.
:>json string|null application: The application used by the client.
:>json string|null application_locale: The locale used by the client for the application.
:>json string|null client_id: The client's hashed telemetry ID.
:>json string|null install_date: The add-on install date.
:>json string|null operating_system: The client's operating system.
:>json string|null operating_system_version: The client's operating system version.
:>json string|null reason: The reason for the report.
------------------------------
@ -47,7 +75,7 @@ Submitting a user abuse report
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.
so reports can be responded to if necessary.
.. http:post:: /api/v4/abuse/report/user/

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

@ -14,6 +14,41 @@ from olympia.users.models import UserProfile
@python_2_unicode_compatible
class AbuseReport(ModelBase):
# Note: those choices don't need to be translated for now, the
# human-readable values are only exposed in the admin. The values will be
# updated once they are finalized in the PRD.
ADDON_SIGNATURE_CHOICES = (
(None, 'None'),
)
REASON_CHOICES = (
(None, 'None'),
(1, 'Malware'),
(2, 'Spam / Advertising'),
(3, 'Search takeover'),
(4, 'New tab takeover'),
(5, 'Breaks websites'),
(6, 'Offensive'),
(7, 'Doesn\'t match description'),
(8, 'Doesn\'t work'),
)
REASON_CHOICES_API = {
None: None,
1: 'malware',
2: 'spam_or_advertising',
3: 'search_takeover',
4: 'new_tab_takeover',
5: 'breaks_websites',
6: 'offensive',
7: 'does_not_match_description',
8: 'does_not_work',
}
ADDON_INSTALL_METHOD_CHOICES = (
(None, 'None'),
)
ADDON_INSTALL_ENTRY_POINTS_CHOICES = (
(None, 'None'),
)
# NULL if the reporter is anonymous.
reporter = models.ForeignKey(
UserProfile, null=True, blank=True, related_name='abuse_reported',
@ -33,6 +68,43 @@ class AbuseReport(ModelBase):
on_delete=models.SET_NULL)
message = models.TextField()
# Extra optional fields for more information, giving some context that is
# meant to be extracted automatically by the client (i.e. Firefox) and
# submitted via the API.
client_id = models.CharField(
default=None, max_length=64, blank=True, null=True)
addon_name = models.CharField(
default=None, max_length=255, blank=True, null=True)
addon_summary = models.CharField(
default=None, max_length=255, blank=True, null=True)
addon_version = models.CharField(
default=None, max_length=255, blank=True, null=True)
addon_signature = models.PositiveSmallIntegerField(
default=None, choices=ADDON_SIGNATURE_CHOICES, blank=True, null=True)
application = models.PositiveSmallIntegerField(
default=amo.FIREFOX.id, choices=amo.APPS_CHOICES, blank=True,
null=True)
application_version = models.CharField(
default=None, max_length=255, blank=True, null=True)
application_locale = models.CharField(
default=None, max_length=255, blank=True, null=True)
operating_system = models.CharField(
default=None, max_length=255, blank=True, null=True)
operating_system_version = models.CharField(
default=None, max_length=255, blank=True, null=True)
install_date = models.DateTimeField(
default=None, blank=True, null=True)
reason = models.PositiveSmallIntegerField(
default=None, choices=REASON_CHOICES, blank=True, null=True)
addon_install_origin = models.CharField(
default=None, max_length=255, blank=True, null=True)
addon_install_method = models.PositiveSmallIntegerField(
default=None, choices=ADDON_INSTALL_METHOD_CHOICES, blank=True,
null=True)
addon_install_entry_point = models.PositiveSmallIntegerField(
default=None, choices=ADDON_INSTALL_ENTRY_POINTS_CHOICES, blank=True,
null=True)
class Meta:
db_table = 'abuse_reports'

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

@ -1,7 +1,9 @@
from rest_framework import serializers
from olympia import amo
from olympia.abuse.models import AbuseReport
from olympia.accounts.serializers import BaseUserSerializer
from olympia.api.fields import ReverseChoiceField
class BaseAbuseReportSerializer(serializers.ModelSerializer):
@ -29,11 +31,30 @@ class BaseAbuseReportSerializer(serializers.ModelSerializer):
class AddonAbuseReportSerializer(BaseAbuseReportSerializer):
addon = serializers.SerializerMethodField()
reason = ReverseChoiceField(
choices=list(AbuseReport.REASON_CHOICES_API.items()), required=False)
application = ReverseChoiceField(
choices=list((v.id, k) for k, v in amo.APPS.items()), required=False)
class Meta:
model = AbuseReport
fields = BaseAbuseReportSerializer.Meta.fields + (
'addon',
'addon_install_entry_point',
'addon_install_method',
'addon_install_origin',
'addon_name',
'addon_signature',
'addon_summary',
'addon_version',
'application',
'application_locale',
'application_version',
'client_id',
'install_date',
'operating_system',
'operating_system_version',
'reason',
)
def to_internal_value(self, data):

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

@ -18,7 +18,22 @@ class TestAddonAbuseReportSerializer(TestCase):
'addon': {'guid': addon.guid,
'id': addon.id,
'slug': addon.slug},
'message': 'bad stuff'}
'message': 'bad stuff',
'addon_install_entry_point': None,
'addon_install_method': None,
'addon_install_origin': None,
'addon_name': None,
'addon_signature': None,
'addon_summary': None,
'addon_version': None,
'application': 'firefox',
'application_locale': None,
'application_version': None,
'client_id': None,
'install_date': None,
'operating_system': None,
'operating_system_version': None,
'reason': None}
def test_guid_report(self):
report = AbuseReport(guid='@guid', message='bad stuff')
@ -27,7 +42,22 @@ class TestAddonAbuseReportSerializer(TestCase):
'addon': {'guid': '@guid',
'id': None,
'slug': None},
'message': 'bad stuff'}
'message': 'bad stuff',
'addon_install_entry_point': None,
'addon_install_method': None,
'addon_install_origin': None,
'addon_name': None,
'addon_signature': None,
'addon_summary': None,
'addon_version': None,
'application': 'firefox',
'application_locale': None,
'application_version': None,
'client_id': None,
'install_date': None,
'operating_system': None,
'operating_system_version': None,
'reason': None}
class TestUserAbuseReportSerializer(TestCase):

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

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import json
from datetime import datetime
from django.core import mail
@ -138,6 +140,102 @@ class AddonAbuseViewSetTestBase(object):
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 429
def test_optional_fields(self):
# FIXME: when choices for addon_signature, addon_install_method and
# addon_install_entry_point are finalized, update this test. In the
# meantime we pass None.
data = {
'addon': '@mysteryaddon',
'message': u'This is abusé!',
'client_id': 'i' * 64,
'addon_name': u'Addon Næme',
'addon_summary': u'Addon sûmmary',
'addon_version': '0.01.01',
'addon_signature': None,
'application': 'firefox',
'application_version': '42.0.1',
'application_locale': u'Lô-käl',
'operating_system': u'Sømething OS',
'install_date': '2004-08-15T16:23:42',
'reason': 'spam_or_advertising',
'addon_install_origin': 'http://example.com/',
'addon_install_method': None,
'addon_install_entry_point': None,
}
response = self.client.post(
self.url,
data=data,
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 201, response.content
assert AbuseReport.objects.filter(guid=data['addon']).exists()
report = AbuseReport.objects.get(guid=data['addon'])
self.check_report(
report, u'[Addon] Abuse Report for %s' % data['addon'])
assert not report.addon # Not an add-on in database, that's ok.
# Straightforward comparisons:
for field in ('message', 'client_id', 'addon_name', 'addon_summary',
'addon_version', 'application_version',
'application_locale', 'operating_system',
'addon_install_origin'):
assert getattr(report, field) == data[field], field
# More complex comparisons:
assert report.addon_signature is None
assert report.application == amo.FIREFOX.id
assert report.install_date == datetime(2004, 8, 15, 16, 23, 42)
assert report.reason == 2 # Spam / Advertising
assert report.addon_install_method is None
assert report.addon_install_entry_point is None
def test_optional_fields_errors(self):
data = {
'addon': '@mysteryaddon',
'message': u'Message cân be quite big if needed' * 256,
'client_id': 'i' * 65,
'addon_name': 'a' * 256,
'addon_summary': 's' * 256,
'addon_version': 'v' * 256,
'addon_signature': 'Something not in signature choices',
'application': 'FIRE! EXCLAMATION MARK',
'application_version': '1' * 256,
'application_locale': 'l' * 256,
'operating_system': 'o' * 256,
'install_date': 'not_a_date',
'reason': 'Something not in reason choices',
'addon_install_origin': 'u' * 256,
'addon_install_method': 'Something not in install method choices',
'addon_install_entry_point': 'Something not in entrypoint choices',
}
response = self.client.post(
self.url,
data=data,
REMOTE_ADDR='123.45.67.89')
assert response.status_code == 400
expected_max_length_message = (
'Ensure this field has no more than %d characters.')
expected_choices_message = '"%s" is not a valid choice.'
assert response.json() == {
'client_id': [expected_max_length_message % 64],
'addon_name': [expected_max_length_message % 255],
'addon_summary': [expected_max_length_message % 255],
'addon_version': [expected_max_length_message % 255],
'addon_signature': [
expected_choices_message % data['addon_signature']],
'application': [expected_choices_message % data['application']],
'application_version': [expected_max_length_message % 255],
'application_locale': [expected_max_length_message % 255],
'operating_system': [expected_max_length_message % 255],
'install_date': [
'Datetime has wrong format. Use one of these formats '
'instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
'reason': [expected_choices_message % data['reason']],
'addon_install_origin': [expected_max_length_message % 255],
'addon_install_method': [
expected_choices_message % data['addon_install_method']],
'addon_install_entry_point': [
expected_choices_message % data['addon_install_entry_point']],
}
class TestAddonAbuseViewSetLoggedOut(AddonAbuseViewSetTestBase, TestCase):
def check_reporter(self, report):

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

@ -1,12 +1,8 @@
from django.http import Http404
from rest_framework import status
from rest_framework.exceptions import ParseError
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from olympia.abuse.models import AbuseReport
from olympia.abuse.serializers import (
AddonAbuseReportSerializer, UserAbuseReportSerializer)
from olympia.accounts.views import AccountViewSet

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

@ -0,0 +1,16 @@
ALTER TABLE `abuse_reports`
ADD COLUMN `client_id` varchar(64),
ADD COLUMN `addon_name` varchar(255),
ADD COLUMN `addon_summary` varchar(255),
ADD COLUMN `addon_version` varchar(255),
ADD COLUMN `addon_signature` smallint UNSIGNED,
ADD COLUMN `application` smallint UNSIGNED,
ADD COLUMN `application_version` varchar(255),
ADD COLUMN `application_locale` varchar(255),
ADD COLUMN `operating_system` varchar(255),
ADD COLUMN `operating_system_version` varchar(255),
ADD COLUMN `install_date` datetime(6),
ADD COLUMN `reason` smallint UNSIGNED,
ADD COLUMN `addon_install_origin` varchar(255),
ADD COLUMN `addon_install_method` smallint UNSIGNED,
ADD COLUMN `addon_install_entry_point` smallint UNSIGNED;