Hold actions on the back of decisions, rather than process them, if content is high profile (#22766)

* seperate decision from action enforcement

* implement hold actions for BAN_USER

* implement hold actions for DELETE_RATING

* implement hold action for DELETE_COLLECTION

* migration number clash

* implement old actions for DISABLE_ADDON
This commit is contained in:
Andrew Williamson 2024-10-18 11:24:26 +01:00 коммит произвёл GitHub
Родитель 3a7d24badb
Коммит 8b100c94ae
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 545 добавлений и 111 удалений

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

@ -0,0 +1,19 @@
# Generated by Django 4.2.16 on 2024-10-14 13:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('abuse', '0040_alter_cinderpolicy_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='cinderdecision',
name='date',
field=models.DateTimeField(db_column='date', null=True),
),
migrations.RenameField(model_name='cinderdecision', old_name='date', new_name='action_date'),
]

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

@ -5,7 +5,6 @@ from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from olympia import amo from olympia import amo
@ -278,14 +277,13 @@ class CinderJob(ModelBase):
self, self,
*, *,
decision_cinder_id, decision_cinder_id,
decision_date,
decision_action, decision_action,
decision_notes, decision_notes,
policy_ids, policy_ids,
): ):
"""This is called for cinder originated decisions. """This is called for cinder originated decisions.
See resolve_job for reviewer tools originated decisions.""" See resolve_job for reviewer tools originated decisions."""
overriden_action = getattr(self.decision, 'action', None) overridden_action = getattr(self.decision, 'action', None)
# We need either an AbuseReport or CinderDecision for the target props # We need either an AbuseReport or CinderDecision for the target props
abuse_report_or_decision = ( abuse_report_or_decision = (
self.appealed_decisions.first() or self.abusereport_set.first() self.appealed_decisions.first() or self.abusereport_set.first()
@ -301,7 +299,6 @@ class CinderJob(ModelBase):
'rating': abuse_report_or_decision.rating, 'rating': abuse_report_or_decision.rating,
'collection': abuse_report_or_decision.collection, 'collection': abuse_report_or_decision.collection,
'user': abuse_report_or_decision.user, 'user': abuse_report_or_decision.user,
'date': decision_date,
'cinder_id': decision_cinder_id, 'cinder_id': decision_cinder_id,
'action': decision_action, 'action': decision_action,
'notes': decision_notes[ 'notes': decision_notes[
@ -313,14 +310,8 @@ class CinderJob(ModelBase):
policies = CinderPolicy.objects.filter( policies = CinderPolicy.objects.filter(
uuid__in=policy_ids uuid__in=policy_ids
).without_parents_if_their_children_are_present() ).without_parents_if_their_children_are_present()
self.decision.policies.add(*policies) cinder_decision.policies.add(*policies)
action_helper = self.decision.get_action_helper( cinder_decision.process_action(overridden_action)
overriden_action=overriden_action,
appealed_action=getattr(self.appealed_decisions.first(), 'action', None),
)
log_entry = action_helper.process_action()
self.notify_reporters(action_helper)
action_helper.notify_owners(log_entry_id=getattr(log_entry, 'id', None))
def resolve_job(self, *, log_entry): def resolve_job(self, *, log_entry):
"""This is called for reviewer tools originated decisions. """This is called for reviewer tools originated decisions.
@ -349,7 +340,7 @@ class CinderJob(ModelBase):
appealed_action=getattr(self.appealed_decisions.first(), 'action', None), appealed_action=getattr(self.appealed_decisions.first(), 'action', None),
) )
self.update(decision=cinder_decision) self.update(decision=cinder_decision)
if self.decision.is_delayed: if cinder_decision.is_delayed:
version_list = log_entry.versionlog_set.values_list('version', flat=True) version_list = log_entry.versionlog_set.values_list('version', flat=True)
self.pending_rejections.add( self.pending_rejections.add(
*VersionReviewerFlags.objects.filter(version__in=version_list) *VersionReviewerFlags.objects.filter(version__in=version_list)
@ -920,7 +911,7 @@ class CinderPolicy(ModelBase):
class CinderDecision(ModelBase): class CinderDecision(ModelBase):
action = models.PositiveSmallIntegerField(choices=DECISION_ACTIONS.choices) action = models.PositiveSmallIntegerField(choices=DECISION_ACTIONS.choices)
cinder_id = models.CharField(max_length=36, default=None, null=True, unique=True) cinder_id = models.CharField(max_length=36, default=None, null=True, unique=True)
date = models.DateTimeField(default=timezone.now) action_date = models.DateTimeField(null=True, db_column='date')
notes = models.TextField(max_length=1000, blank=True) notes = models.TextField(max_length=1000, blank=True)
policies = models.ManyToManyField(to='abuse.CinderPolicy') policies = models.ManyToManyField(to='abuse.CinderPolicy')
appeal_job = models.ForeignKey( appeal_job = models.ForeignKey(
@ -1022,7 +1013,7 @@ class CinderDecision(ModelBase):
DECISION_ACTIONS.AMO_CLOSED_NO_ACTION: CinderActionAlreadyRemoved, DECISION_ACTIONS.AMO_CLOSED_NO_ACTION: CinderActionAlreadyRemoved,
}.get(decision_action, CinderActionNotImplemented) }.get(decision_action, CinderActionNotImplemented)
def get_action_helper(self, *, overriden_action=None, appealed_action=None): def get_action_helper(self, *, overridden_action=None, appealed_action=None):
# Base case when it's a new decision, that wasn't an appeal # Base case when it's a new decision, that wasn't an appeal
CinderActionClass = self.get_action_helper_class(self.action) CinderActionClass = self.get_action_helper_class(self.action)
skip_reporter_notify = False skip_reporter_notify = False
@ -1038,11 +1029,11 @@ class CinderDecision(ModelBase):
CinderActionClass = CinderActionTargetAppealRemovalAffirmation CinderActionClass = CinderActionTargetAppealRemovalAffirmation
# (a reporter appeal doesn't need any alternate CinderAction class) # (a reporter appeal doesn't need any alternate CinderAction class)
elif overriden_action in DECISION_ACTIONS.REMOVING: elif overridden_action in DECISION_ACTIONS.REMOVING:
# override on a decision that was a takedown before, and wasn't an appeal # override on a decision that was a takedown before, and wasn't an appeal
if self.action in DECISION_ACTIONS.APPROVING: if self.action in DECISION_ACTIONS.APPROVING:
CinderActionClass = CinderActionOverrideApprove CinderActionClass = CinderActionOverrideApprove
if self.action == overriden_action: if self.action == overridden_action:
# For an override that is still a takedown we can send the same emails # For an override that is still a takedown we can send the same emails
# to the target; but we don't want to notify the reporter again. # to the target; but we don't want to notify the reporter again.
skip_reporter_notify = True skip_reporter_notify = True
@ -1059,8 +1050,8 @@ class CinderDecision(ModelBase):
""" """
now = datetime.now() now = datetime.now()
base_criteria = ( base_criteria = (
self.date self.action_date
and self.date >= now - timedelta(days=APPEAL_EXPIRATION_DAYS) and self.action_date >= now - timedelta(days=APPEAL_EXPIRATION_DAYS)
# Can never appeal an original decision that has been appealed and # Can never appeal an original decision that has been appealed and
# for which we already have a new decision. In some cases the # for which we already have a new decision. In some cases the
# appealed decision (new decision id) can be appealed by the author # appealed decision (new decision id) can be appealed by the author
@ -1187,7 +1178,7 @@ class CinderDecision(ModelBase):
'Missing or invalid cinder_action in activity log details passed to ' 'Missing or invalid cinder_action in activity log details passed to '
'notify_reviewer_decision' 'notify_reviewer_decision'
) )
overriden_action = self.action overridden_action = self.action
self.action = DECISION_ACTIONS.for_constant( self.action = DECISION_ACTIONS.for_constant(
log_entry.details['cinder_action'] log_entry.details['cinder_action']
).value ).value
@ -1201,7 +1192,7 @@ class CinderDecision(ModelBase):
'reasoning': self.notes, 'reasoning': self.notes,
'policy_uuids': [policy.uuid for policy in policies], 'policy_uuids': [policy.uuid for policy in policies],
} }
if not overriden_action and ( if not overridden_action and (
cinder_job := getattr(self, 'cinder_job', None) cinder_job := getattr(self, 'cinder_job', None)
): ):
decision_cinder_id = entity_helper.create_job_decision( decision_cinder_id = entity_helper.create_job_decision(
@ -1211,11 +1202,12 @@ class CinderDecision(ModelBase):
decision_cinder_id = entity_helper.create_decision(**create_decision_kw) decision_cinder_id = entity_helper.create_decision(**create_decision_kw)
with atomic(): with atomic():
self.cinder_id = decision_cinder_id self.cinder_id = decision_cinder_id
self.action_date = datetime.now()
self.save() self.save()
self.policies.set(policies) self.policies.set(policies)
action_helper = self.get_action_helper( action_helper = self.get_action_helper(
overriden_action=overriden_action, appealed_action=appealed_action overridden_action=overridden_action, appealed_action=appealed_action
) )
if cinder_job := getattr(self, 'cinder_job', None): if cinder_job := getattr(self, 'cinder_job', None):
cinder_job.notify_reporters(action_helper) cinder_job.notify_reporters(action_helper)
@ -1249,6 +1241,29 @@ class CinderDecision(ModelBase):
}, },
) )
def process_action(self, overridden_action=None):
"""currently only called by decisions from cinder.
see https://mozilla-hub.atlassian.net/browse/AMOENG-1125
"""
appealed_action = (
getattr(self.cinder_job.appealed_decisions.first(), 'action', None)
if hasattr(self, 'cinder_job')
else None
)
action_helper = self.get_action_helper(
overridden_action=overridden_action,
appealed_action=appealed_action,
)
if not action_helper.should_hold_action():
log_entry = action_helper.process_action()
if cinder_job := getattr(self, 'cinder_job', None):
cinder_job.notify_reporters(action_helper)
action_helper.notify_owners(log_entry_id=getattr(log_entry, 'id', None))
self.update(action_date=datetime.now())
else:
action_helper.hold_action()
class CinderAppeal(ModelBase): class CinderAppeal(ModelBase):
text = models.TextField(blank=False, help_text='The content of the appeal.') text = models.TextField(blank=False, help_text='The content of the appeal.')

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

@ -31,6 +31,8 @@ from olympia.constants.abuse import (
ILLEGAL_CATEGORIES, ILLEGAL_CATEGORIES,
ILLEGAL_SUBCATEGORIES, ILLEGAL_SUBCATEGORIES,
) )
from olympia.constants.promoted import RECOMMENDED
from olympia.core import set_user
from olympia.ratings.models import Rating from olympia.ratings.models import Rating
from olympia.reviewers.models import NeedsHumanReview from olympia.reviewers.models import NeedsHumanReview
from olympia.versions.models import Version, VersionReviewerFlags from olympia.versions.models import Version, VersionReviewerFlags
@ -1172,7 +1174,6 @@ class TestCinderJob(TestCase):
cinder_job = CinderJob.objects.create(job_id='1234') cinder_job = CinderJob.objects.create(job_id='1234')
target = user_factory() target = user_factory()
AbuseReport.objects.create(user=target, cinder_job=cinder_job) AbuseReport.objects.create(user=target, cinder_job=cinder_job)
new_date = datetime(2023, 1, 1)
policy_a = CinderPolicy.objects.create(uuid='123-45', name='aaa', text='AAA') policy_a = CinderPolicy.objects.create(uuid='123-45', name='aaa', text='AAA')
policy_b = CinderPolicy.objects.create(uuid='678-90', name='bbb', text='BBB') policy_b = CinderPolicy.objects.create(uuid='678-90', name='bbb', text='BBB')
@ -1184,13 +1185,11 @@ class TestCinderJob(TestCase):
action_mock.return_value = (True, mock.Mock(id=999)) action_mock.return_value = (True, mock.Mock(id=999))
cinder_job.process_decision( cinder_job.process_decision(
decision_cinder_id='12345', decision_cinder_id='12345',
decision_date=new_date,
decision_action=DECISION_ACTIONS.AMO_BAN_USER.value, decision_action=DECISION_ACTIONS.AMO_BAN_USER.value,
decision_notes='teh notes', decision_notes='teh notes',
policy_ids=['123-45', '678-90'], policy_ids=['123-45', '678-90'],
) )
assert cinder_job.decision.cinder_id == '12345' assert cinder_job.decision.cinder_id == '12345'
assert cinder_job.decision.date == new_date
assert cinder_job.decision.action == DECISION_ACTIONS.AMO_BAN_USER assert cinder_job.decision.action == DECISION_ACTIONS.AMO_BAN_USER
assert cinder_job.decision.notes == 'teh notes' assert cinder_job.decision.notes == 'teh notes'
assert cinder_job.decision.user == target assert cinder_job.decision.user == target
@ -1202,7 +1201,6 @@ class TestCinderJob(TestCase):
cinder_job = CinderJob.objects.create(job_id='1234') cinder_job = CinderJob.objects.create(job_id='1234')
target = user_factory() target = user_factory()
AbuseReport.objects.create(user=target, cinder_job=cinder_job) AbuseReport.objects.create(user=target, cinder_job=cinder_job)
new_date = datetime(2023, 1, 1)
parent_policy = CinderPolicy.objects.create( parent_policy = CinderPolicy.objects.create(
uuid='678-90', name='bbb', text='BBB' uuid='678-90', name='bbb', text='BBB'
) )
@ -1218,13 +1216,11 @@ class TestCinderJob(TestCase):
action_mock.return_value = (True, None) action_mock.return_value = (True, None)
cinder_job.process_decision( cinder_job.process_decision(
decision_cinder_id='12345', decision_cinder_id='12345',
decision_date=new_date,
decision_action=DECISION_ACTIONS.AMO_BAN_USER.value, decision_action=DECISION_ACTIONS.AMO_BAN_USER.value,
decision_notes='teh notes', decision_notes='teh notes',
policy_ids=['123-45', '678-90'], policy_ids=['123-45', '678-90'],
) )
assert cinder_job.decision.cinder_id == '12345' assert cinder_job.decision.cinder_id == '12345'
assert cinder_job.decision.date == new_date
assert cinder_job.decision.action == DECISION_ACTIONS.AMO_BAN_USER assert cinder_job.decision.action == DECISION_ACTIONS.AMO_BAN_USER
assert cinder_job.decision.notes == 'teh notes' assert cinder_job.decision.notes == 'teh notes'
assert cinder_job.decision.user == target assert cinder_job.decision.user == target
@ -1237,7 +1233,6 @@ class TestCinderJob(TestCase):
cinder_job = CinderJob.objects.create(job_id='1234', target_addon=addon) cinder_job = CinderJob.objects.create(job_id='1234', target_addon=addon)
report = AbuseReport.objects.create(guid=addon.guid, cinder_job=cinder_job) report = AbuseReport.objects.create(guid=addon.guid, cinder_job=cinder_job)
assert not cinder_job.resolvable_in_reviewer_tools assert not cinder_job.resolvable_in_reviewer_tools
new_date = datetime(2024, 1, 1)
responses.add( responses.add(
responses.POST, responses.POST,
f'{settings.CINDER_SERVER_URL}create_report', f'{settings.CINDER_SERVER_URL}create_report',
@ -1247,7 +1242,6 @@ class TestCinderJob(TestCase):
cinder_job.process_decision( cinder_job.process_decision(
decision_cinder_id='12345', decision_cinder_id='12345',
decision_date=new_date,
decision_action=DECISION_ACTIONS.AMO_ESCALATE_ADDON, decision_action=DECISION_ACTIONS.AMO_ESCALATE_ADDON,
decision_notes='blah', decision_notes='blah',
policy_ids=[], policy_ids=[],
@ -1325,7 +1319,7 @@ class TestCinderJob(TestCase):
assert 'entity' not in request_body assert 'entity' not in request_body
cinder_job.reload() cinder_job.reload()
assert cinder_job.decision.action == cinder_action assert cinder_job.decision.action == cinder_action
self.assertCloseToNow(cinder_job.decision.date) self.assertCloseToNow(cinder_job.decision.action_date)
assert list(cinder_job.decision.policies.all()) == policies assert list(cinder_job.decision.policies.all()) == policies
assert len(mail.outbox) == (2 if expect_target_email else 1) assert len(mail.outbox) == (2 if expect_target_email else 1)
assert mail.outbox[0].to == [abuse_report.reporter.email] assert mail.outbox[0].to == [abuse_report.reporter.email]
@ -1399,7 +1393,7 @@ class TestCinderJob(TestCase):
assert cinder_job.decision.action == ( assert cinder_job.decision.action == (
DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON
) )
self.assertCloseToNow(cinder_job.decision.date) self.assertCloseToNow(cinder_job.decision.action_date)
assert list(cinder_job.decision.policies.all()) == policies assert list(cinder_job.decision.policies.all()) == policies
assert set(cinder_job.pending_rejections.all()) == set( assert set(cinder_job.pending_rejections.all()) == set(
VersionReviewerFlags.objects.filter( VersionReviewerFlags.objects.filter(
@ -1470,7 +1464,7 @@ class TestCinderJob(TestCase):
assert 'entity' not in request_body assert 'entity' not in request_body
appeal_job.reload() appeal_job.reload()
assert appeal_job.decision.action == DECISION_ACTIONS.AMO_DISABLE_ADDON assert appeal_job.decision.action == DECISION_ACTIONS.AMO_DISABLE_ADDON
self.assertCloseToNow(appeal_job.decision.date) self.assertCloseToNow(appeal_job.decision.action_date)
assert list(appeal_job.decision.policies.all()) == policies assert list(appeal_job.decision.policies.all()) == policies
assert len(mail.outbox) == 1 assert len(mail.outbox) == 1
@ -1602,7 +1596,7 @@ class TestCinderJob(TestCase):
assert request_body['reasoning'] == 'some review text' assert request_body['reasoning'] == 'some review text'
cinder_job.reload() cinder_job.reload()
assert cinder_job.decision.action == DECISION_ACTIONS.AMO_DISABLE_ADDON assert cinder_job.decision.action == DECISION_ACTIONS.AMO_DISABLE_ADDON
self.assertCloseToNow(cinder_job.decision.date) self.assertCloseToNow(cinder_job.decision.action_date)
assert list(cinder_job.decision.policies.all()) == policies assert list(cinder_job.decision.policies.all()) == policies
assert len(mail.outbox) == 2 assert len(mail.outbox) == 2
assert mail.outbox[0].to == [abuse_report.reporter.email] assert mail.outbox[0].to == [abuse_report.reporter.email]
@ -1797,6 +1791,7 @@ class TestCinderDecisionCanBeAppealed(TestCase):
cinder_id='fake_decision_id', cinder_id='fake_decision_id',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=self.addon, addon=self.addon,
action_date=datetime.now(),
) )
def test_appealed_decision_already_made(self): def test_appealed_decision_already_made(self):
@ -1937,6 +1932,7 @@ class TestCinderDecisionCanBeAppealed(TestCase):
cinder_id='fake_appeal_decision_id', cinder_id='fake_appeal_decision_id',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=self.addon, addon=self.addon,
action_date=datetime.now(),
), ),
) )
report = AbuseReport.objects.create( report = AbuseReport.objects.create(
@ -1970,7 +1966,22 @@ class TestCinderDecisionCanBeAppealed(TestCase):
assert self.decision.can_be_appealed( assert self.decision.can_be_appealed(
is_reporter=True, abuse_report=initial_report is_reporter=True, abuse_report=initial_report
) )
self.decision.update(date=self.days_ago(APPEAL_EXPIRATION_DAYS + 1)) self.decision.update(action_date=self.days_ago(APPEAL_EXPIRATION_DAYS + 1))
assert not self.decision.can_be_appealed(
is_reporter=True, abuse_report=initial_report
)
def test_reporter_cant_appeal_when_no_action_date(self):
initial_report = AbuseReport.objects.create(
guid=self.addon.guid,
cinder_job=CinderJob.objects.create(decision=self.decision),
reporter=self.reporter,
reason=AbuseReport.REASONS.ILLEGAL,
)
assert self.decision.can_be_appealed(
is_reporter=True, abuse_report=initial_report
)
self.decision.update(action_date=None)
assert not self.decision.can_be_appealed( assert not self.decision.can_be_appealed(
is_reporter=True, abuse_report=initial_report is_reporter=True, abuse_report=initial_report
) )
@ -2021,6 +2032,7 @@ class TestCinderDecisionCanBeAppealed(TestCase):
cinder_id='fake_appeal_decision_id', cinder_id='fake_appeal_decision_id',
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
addon=self.addon, addon=self.addon,
action_date=datetime.now(),
), ),
) )
self.decision.update(appeal_job=appeal_job) self.decision.update(appeal_job=appeal_job)
@ -2148,6 +2160,12 @@ class TestCinderPolicy(TestCase):
@override_switch('dsa-abuse-reports-review', active=True) @override_switch('dsa-abuse-reports-review', active=True)
@override_switch('dsa-appeals-review', active=True) @override_switch('dsa-appeals-review', active=True)
class TestCinderDecision(TestCase): class TestCinderDecision(TestCase):
def setUp(self):
# It's the webhook's responsibility to do this before calling the
# action. We need it for the ActivityLog creation to work.
self.task_user = user_factory(pk=settings.TASK_USER_ID)
set_user(self.task_user)
def test_get_reference_id(self): def test_get_reference_id(self):
decision = CinderDecision() decision = CinderDecision()
assert decision.get_reference_id() == 'NoClass#None' assert decision.get_reference_id() == 'NoClass#None'
@ -2292,7 +2310,7 @@ class TestCinderDecision(TestCase):
} }
) )
helper = decision.get_action_helper( helper = decision.get_action_helper(
appealed_action=appealed_action, overriden_action=overridden_action appealed_action=appealed_action, overridden_action=overridden_action
) )
assert helper.__class__ == ActionClass assert helper.__class__ == ActionClass
assert helper.decision == decision assert helper.decision == decision
@ -2321,7 +2339,7 @@ class TestCinderDecision(TestCase):
} }
) )
helper = decision.get_action_helper( helper = decision.get_action_helper(
appealed_action=None, overriden_action=overridden_action appealed_action=None, overridden_action=overridden_action
) )
assert helper.reporter_template_path is None assert helper.reporter_template_path is None
assert helper.reporter_appeal_template_path is None assert helper.reporter_appeal_template_path is None
@ -2329,7 +2347,6 @@ class TestCinderDecision(TestCase):
assert ActionClass.reporter_appeal_template_path is not None assert ActionClass.reporter_appeal_template_path is not None
def _test_appeal_as_target(self, *, resolvable_in_reviewer_tools): def _test_appeal_as_target(self, *, resolvable_in_reviewer_tools):
user_factory(id=settings.TASK_USER_ID)
addon = addon_factory( addon = addon_factory(
status=amo.STATUS_DISABLED, status=amo.STATUS_DISABLED,
file_kw={'is_signed': True, 'status': amo.STATUS_DISABLED}, file_kw={'is_signed': True, 'status': amo.STATUS_DISABLED},
@ -2343,7 +2360,7 @@ class TestCinderDecision(TestCase):
resolvable_in_reviewer_tools=resolvable_in_reviewer_tools, resolvable_in_reviewer_tools=resolvable_in_reviewer_tools,
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
addon=addon, addon=addon,
), ),
@ -2405,7 +2422,7 @@ class TestCinderDecision(TestCase):
cinder_job=CinderJob.objects.create( cinder_job=CinderJob.objects.create(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
addon=addon, addon=addon,
), ),
@ -2443,7 +2460,7 @@ class TestCinderDecision(TestCase):
cinder_job=CinderJob.objects.create( cinder_job=CinderJob.objects.create(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
# This (target is an add-on, decision is a user ban) shouldn't # This (target is an add-on, decision is a user ban) shouldn't
# be possible but we want to make sure this is handled # be possible but we want to make sure this is handled
# explicitly. # explicitly.
@ -2487,7 +2504,7 @@ class TestCinderDecision(TestCase):
cinder_job=CinderJob.objects.create( cinder_job=CinderJob.objects.create(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
action=DECISION_ACTIONS.AMO_BAN_USER, action=DECISION_ACTIONS.AMO_BAN_USER,
user=target, user=target,
) )
@ -2528,7 +2545,7 @@ class TestCinderDecision(TestCase):
target_addon=addon, target_addon=addon,
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon, addon=addon,
), ),
@ -2572,7 +2589,7 @@ class TestCinderDecision(TestCase):
target_addon=addon, target_addon=addon,
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon, addon=addon,
), ),
@ -2615,7 +2632,7 @@ class TestCinderDecision(TestCase):
cinder_job = CinderJob.objects.create( cinder_job = CinderJob.objects.create(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon_factory(), addon=addon_factory(),
) )
@ -2638,7 +2655,7 @@ class TestCinderDecision(TestCase):
cinder_job = CinderJob.objects.create( cinder_job = CinderJob.objects.create(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='4815162342-lost', cinder_id='4815162342-lost',
date=self.days_ago(179), action_date=self.days_ago(179),
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon, addon=addon,
) )
@ -2715,7 +2732,7 @@ class TestCinderDecision(TestCase):
assert request_body['enforcement_actions_slugs'] == [ assert request_body['enforcement_actions_slugs'] == [
cinder_action.api_value cinder_action.api_value
] ]
self.assertCloseToNow(decision.date) self.assertCloseToNow(decision.action_date)
assert list(decision.policies.all()) == policies assert list(decision.policies.all()) == policies
assert CinderDecision.objects.count() == 1 assert CinderDecision.objects.count() == 1
assert decision.id assert decision.id
@ -2730,7 +2747,7 @@ class TestCinderDecision(TestCase):
assert request_body['enforcement_actions_slugs'] == [ assert request_body['enforcement_actions_slugs'] == [
cinder_action.api_value cinder_action.api_value
] ]
self.assertCloseToNow(decision.date) self.assertCloseToNow(decision.action_date)
assert list(decision.policies.all()) == policies assert list(decision.policies.all()) == policies
assert CinderDecision.objects.count() == 1 assert CinderDecision.objects.count() == 1
assert decision.id assert decision.id
@ -2995,6 +3012,130 @@ class TestCinderDecision(TestCase):
not in mail.outbox[0].body not in mail.outbox[0].body
) )
def test_process_action_ban_user_held(self):
user = user_factory(email='superstarops@mozilla.com')
decision = CinderDecision.objects.create(
user=user, action=DECISION_ACTIONS.AMO_BAN_USER
)
assert decision.action_date is None
decision.process_action()
assert decision.action_date is None
assert not user.reload().banned
assert (
ActivityLog.objects.filter(
action=amo.LOG.HELD_ACTION_ADMIN_USER_BANNED.id
).count()
== 1
)
def test_process_action_ban_user(self):
user = user_factory()
decision = CinderDecision.objects.create(
user=user, action=DECISION_ACTIONS.AMO_BAN_USER
)
assert decision.action_date is None
decision.process_action()
self.assertCloseToNow(decision.action_date)
self.assertCloseToNow(user.reload().banned)
assert (
ActivityLog.objects.filter(action=amo.LOG.ADMIN_USER_BANNED.id).count() == 1
)
def test_process_action_disable_addon_held(self):
addon = addon_factory()
self.make_addon_promoted(addon, RECOMMENDED, approve_version=True)
decision = CinderDecision.objects.create(
addon=addon, action=DECISION_ACTIONS.AMO_DISABLE_ADDON
)
assert decision.action_date is None
decision.process_action()
assert decision.action_date is None
assert addon.reload().status == amo.STATUS_APPROVED
assert (
ActivityLog.objects.filter(
action=amo.LOG.HELD_ACTION_FORCE_DISABLE.id
).count()
== 1
)
def test_process_action_disable_addon(self):
addon = addon_factory()
decision = CinderDecision.objects.create(
addon=addon, action=DECISION_ACTIONS.AMO_DISABLE_ADDON
)
assert decision.action_date is None
decision.process_action()
self.assertCloseToNow(decision.action_date)
assert addon.reload().status == amo.STATUS_DISABLED
assert ActivityLog.objects.filter(action=amo.LOG.FORCE_DISABLE.id).count() == 1
def test_process_action_delete_collection_held(self):
collection = collection_factory(author=self.task_user)
decision = CinderDecision.objects.create(
collection=collection, action=DECISION_ACTIONS.AMO_DELETE_COLLECTION
)
assert decision.action_date is None
decision.process_action()
assert decision.action_date is None
assert not collection.reload().deleted
assert (
ActivityLog.objects.filter(
action=amo.LOG.HELD_ACTION_COLLECTION_DELETED.id
).count()
== 1
)
def test_process_action_delete_collection(self):
collection = collection_factory(author=user_factory())
decision = CinderDecision.objects.create(
collection=collection, action=DECISION_ACTIONS.AMO_DELETE_COLLECTION
)
assert decision.action_date is None
decision.process_action()
self.assertCloseToNow(decision.action_date)
assert collection.reload().deleted
assert (
ActivityLog.objects.filter(action=amo.LOG.COLLECTION_DELETED.id).count()
== 1
)
def test_process_action_delete_rating_held(self):
user = user_factory()
addon = addon_factory(users=[user])
rating = Rating.objects.create(
addon=addon,
user=user,
body='reply',
reply_to=Rating.objects.create(
addon=addon, user=user_factory(), body='sdsd'
),
)
decision = CinderDecision.objects.create(
rating=rating, action=DECISION_ACTIONS.AMO_DELETE_RATING
)
self.make_addon_promoted(rating.addon, RECOMMENDED, approve_version=True)
assert decision.action_date is None
decision.process_action()
assert decision.action_date is None
assert not rating.reload().deleted
assert (
ActivityLog.objects.filter(
action=amo.LOG.HELD_ACTION_DELETE_RATING.id
).count()
== 1
)
def test_process_action_delete_rating(self):
rating = Rating.objects.create(addon=addon_factory(), user=user_factory())
decision = CinderDecision.objects.create(
rating=rating, action=DECISION_ACTIONS.AMO_DELETE_RATING
)
assert decision.action_date is None
decision.process_action()
self.assertCloseToNow(decision.action_date)
assert rating.reload().deleted
assert ActivityLog.objects.filter(action=amo.LOG.DELETE_RATING.id).count() == 1
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize( @pytest.mark.parametrize(

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

@ -434,6 +434,7 @@ def test_addon_appeal_to_cinder_reporter(statsd_incr_mock):
cinder_id='4815162342-abc', cinder_id='4815162342-abc',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon, addon=addon,
action_date=datetime.now(),
) )
) )
abuse_report = AbuseReport.objects.create( abuse_report = AbuseReport.objects.create(
@ -495,6 +496,7 @@ def test_addon_appeal_to_cinder_reporter_exception(statsd_incr_mock):
cinder_id='4815162342-abc', cinder_id='4815162342-abc',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon, addon=addon,
action_date=datetime.now(),
) )
) )
abuse_report = AbuseReport.objects.create( abuse_report = AbuseReport.objects.create(
@ -536,6 +538,7 @@ def test_addon_appeal_to_cinder_authenticated_reporter():
cinder_id='4815162342-abc', cinder_id='4815162342-abc',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
addon=addon, addon=addon,
action_date=datetime.now(),
) )
) )
abuse_report = AbuseReport.objects.create( abuse_report = AbuseReport.objects.create(
@ -593,6 +596,7 @@ def test_addon_appeal_to_cinder_authenticated_author():
cinder_id='4815162342-abc', cinder_id='4815162342-abc',
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
addon=addon, addon=addon,
action_date=datetime.now(),
) )
responses.add( responses.add(
responses.POST, responses.POST,

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

@ -1,3 +1,5 @@
from datetime import datetime
from django.conf import settings from django.conf import settings
from django.core import mail from django.core import mail
from django.urls import reverse from django.urls import reverse
@ -6,9 +8,10 @@ from waffle.testutils import override_switch
from olympia import amo from olympia import amo
from olympia.activity.models import ActivityLog, ActivityLogToken from olympia.activity.models import ActivityLog, ActivityLogToken
from olympia.addons.models import Addon from olympia.addons.models import Addon, AddonUser
from olympia.amo.tests import TestCase, addon_factory, collection_factory, user_factory from olympia.amo.tests import TestCase, addon_factory, collection_factory, user_factory
from olympia.constants.abuse import DECISION_ACTIONS from olympia.constants.abuse import DECISION_ACTIONS
from olympia.constants.promoted import RECOMMENDED
from olympia.core import set_user from olympia.core import set_user
from olympia.ratings.models import Rating from olympia.ratings.models import Rating
@ -37,22 +40,22 @@ class BaseTestCinderAction:
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
notes="extra note's", notes="extra note's",
addon=addon, addon=addon,
action_date=datetime.now(),
) )
self.cinder_job = CinderJob.objects.create( self.cinder_job = CinderJob.objects.create(
job_id='1234', decision=self.decision job_id='1234', decision=self.decision
) )
self.decision.policies.add( self.policy = CinderPolicy.objects.create(
CinderPolicy.objects.create( uuid='1234',
uuid='1234', name='Bad policy',
name='Bad policy', text='This is bad thing',
text='This is bad thing', parent=CinderPolicy.objects.create(
parent=CinderPolicy.objects.create( uuid='p4r3nt',
uuid='p4r3nt', name='Parent Policy',
name='Parent Policy', text='Parent policy text',
text='Parent policy text', ),
),
)
) )
self.decision.policies.add(self.policy)
self.abuse_report_no_auth = AbuseReport.objects.create( self.abuse_report_no_auth = AbuseReport.objects.create(
reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE, reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE,
guid=addon.guid, guid=addon.guid,
@ -332,14 +335,18 @@ class TestCinderActionUser(BaseTestCinderAction, TestCase):
def _test_ban_user(self): def _test_ban_user(self):
self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER) self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER)
action = self.ActionClass(self.decision) action = self.ActionClass(self.decision)
assert action.process_action() is None activity = action.process_action()
assert activity.log == amo.LOG.ADMIN_USER_BANNED
assert ActivityLog.objects.count() == 1
assert activity.arguments == [self.user, self.policy]
assert activity.user == self.task_user
assert activity.details == {
'comments': self.decision.notes,
'cinder_action': DECISION_ACTIONS.AMO_BAN_USER,
}
self.user.reload() self.user.reload()
self.assertCloseToNow(self.user.banned) self.assertCloseToNow(self.user.banned)
assert ActivityLog.objects.count() == 1
activity = ActivityLog.objects.get(action=amo.LOG.ADMIN_USER_BANNED.id)
assert activity.arguments == [self.user]
assert activity.user == self.task_user
assert len(mail.outbox) == 0 assert len(mail.outbox) == 0
self.cinder_job.notify_reporters(action) self.cinder_job.notify_reporters(action)
@ -420,6 +427,43 @@ class TestCinderActionUser(BaseTestCinderAction, TestCase):
action.notify_owners() action.notify_owners()
self._test_owner_affirmation_email(f'Mozilla Add-ons: {self.user.name}') self._test_owner_affirmation_email(f'Mozilla Add-ons: {self.user.name}')
def test_should_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER)
action = self.ActionClass(self.decision)
assert action.should_hold_action() is False
self.user.update(email='superstarops@mozilla.com')
assert action.should_hold_action() is True
self.user.update(email='foo@baa')
assert action.should_hold_action() is False
del self.user.groups_list
self.grant_permission(self.user, 'this:thing')
assert action.should_hold_action() is True
self.user.groups_list = []
assert action.should_hold_action() is False
addon = addon_factory(users=[self.user])
assert action.should_hold_action() is False
self.make_addon_promoted(addon, RECOMMENDED, approve_version=True)
assert action.should_hold_action() is True
self.user.banned = datetime.now()
assert action.should_hold_action() is False
def test_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER)
action = self.ActionClass(self.decision)
activity = action.hold_action()
assert activity.log == amo.LOG.HELD_ACTION_ADMIN_USER_BANNED
assert ActivityLog.objects.count() == 1
assert activity.arguments == [self.user, self.policy]
assert activity.user == self.task_user
assert activity.details == {
'comments': self.decision.notes,
'cinder_action': DECISION_ACTIONS.AMO_BAN_USER,
}
@override_switch('dsa-cinder-forwarded-review', active=True) @override_switch('dsa-cinder-forwarded-review', active=True)
@override_switch('dsa-appeals-review', active=True) @override_switch('dsa-appeals-review', active=True)
@ -442,7 +486,7 @@ class TestCinderActionAddon(BaseTestCinderAction, TestCase):
assert activity.log == amo.LOG.FORCE_DISABLE assert activity.log == amo.LOG.FORCE_DISABLE
assert self.addon.reload().status == amo.STATUS_DISABLED assert self.addon.reload().status == amo.STATUS_DISABLED
assert ActivityLog.objects.count() == 1 assert ActivityLog.objects.count() == 1
assert activity.arguments == [self.addon] assert activity.arguments == [self.addon, self.policy]
assert activity.user == self.task_user assert activity.user == self.task_user
assert len(mail.outbox) == 0 assert len(mail.outbox) == 0
@ -762,6 +806,30 @@ class TestCinderActionAddon(BaseTestCinderAction, TestCase):
) )
assert 'right to appeal' not in mail_item.body assert 'right to appeal' not in mail_item.body
def test_should_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DISABLE_ADDON)
action = self.ActionClass(self.decision)
assert action.should_hold_action() is False
self.make_addon_promoted(self.addon, RECOMMENDED, approve_version=True)
assert action.should_hold_action() is True
self.addon.status = amo.STATUS_DISABLED
assert action.should_hold_action() is False
def test_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DISABLE_ADDON)
action = self.ActionClass(self.decision)
activity = action.hold_action()
assert activity.log == amo.LOG.HELD_ACTION_FORCE_DISABLE
assert ActivityLog.objects.count() == 1
assert activity.arguments == [self.addon, self.policy]
assert activity.user == self.task_user
assert activity.details == {
'comments': self.decision.notes,
'cinder_action': DECISION_ACTIONS.AMO_DISABLE_ADDON,
}
class TestCinderActionCollection(BaseTestCinderAction, TestCase): class TestCinderActionCollection(BaseTestCinderAction, TestCase):
ActionClass = CinderActionDeleteCollection ActionClass = CinderActionDeleteCollection
@ -788,7 +856,7 @@ class TestCinderActionCollection(BaseTestCinderAction, TestCase):
assert ActivityLog.objects.count() == 1 assert ActivityLog.objects.count() == 1
activity = ActivityLog.objects.get(action=amo.LOG.COLLECTION_DELETED.id) activity = ActivityLog.objects.get(action=amo.LOG.COLLECTION_DELETED.id)
assert activity == log_entry assert activity == log_entry
assert activity.arguments == [self.collection] assert activity.arguments == [self.collection, self.policy]
assert activity.user == self.task_user assert activity.user == self.task_user
assert len(mail.outbox) == 0 assert len(mail.outbox) == 0
@ -870,6 +938,30 @@ class TestCinderActionCollection(BaseTestCinderAction, TestCase):
action.notify_owners() action.notify_owners()
self._test_owner_affirmation_email(f'Mozilla Add-ons: {self.collection.name}') self._test_owner_affirmation_email(f'Mozilla Add-ons: {self.collection.name}')
def test_should_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_COLLECTION)
action = self.ActionClass(self.decision)
assert action.should_hold_action() is False
self.collection.update(author=self.task_user)
assert action.should_hold_action() is True
self.collection.deleted = True
assert action.should_hold_action() is False
def test_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_COLLECTION)
action = self.ActionClass(self.decision)
activity = action.hold_action()
assert activity.log == amo.LOG.HELD_ACTION_COLLECTION_DELETED
assert ActivityLog.objects.count() == 1
assert activity.arguments == [self.collection, self.policy]
assert activity.user == self.task_user
assert activity.details == {
'comments': self.decision.notes,
'cinder_action': DECISION_ACTIONS.AMO_DELETE_COLLECTION,
}
class TestCinderActionRating(BaseTestCinderAction, TestCase): class TestCinderActionRating(BaseTestCinderAction, TestCase):
ActionClass = CinderActionDeleteRating ActionClass = CinderActionDeleteRating
@ -887,13 +979,21 @@ class TestCinderActionRating(BaseTestCinderAction, TestCase):
def _test_delete_rating(self): def _test_delete_rating(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING) self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING)
action = self.ActionClass(self.decision) action = self.ActionClass(self.decision)
assert action.process_action() is None activity = action.process_action()
assert activity.log == amo.LOG.DELETE_RATING
assert ActivityLog.objects.count() == 1
assert activity.arguments == [self.rating, self.policy, self.rating.addon]
assert activity.user == self.task_user
assert activity.details == {
'comments': self.decision.notes,
'cinder_action': DECISION_ACTIONS.AMO_DELETE_RATING,
'addon_id': self.rating.addon_id,
'addon_title': str(self.rating.addon.name),
'body': self.rating.body,
'is_flagged': False,
}
assert self.rating.reload().deleted assert self.rating.reload().deleted
assert ActivityLog.objects.count() == 1
activity = ActivityLog.objects.get(action=amo.LOG.DELETE_RATING.id)
assert activity.arguments == [self.rating.addon, self.rating]
assert activity.user == self.task_user
assert len(mail.outbox) == 0 assert len(mail.outbox) == 0
self.cinder_job.notify_reporters(action) self.cinder_job.notify_reporters(action)
@ -977,3 +1077,35 @@ class TestCinderActionRating(BaseTestCinderAction, TestCase):
self._test_owner_affirmation_email( self._test_owner_affirmation_email(
f'Mozilla Add-ons: "Saying ..." for {self.rating.addon.name}' f'Mozilla Add-ons: "Saying ..." for {self.rating.addon.name}'
) )
def test_should_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING)
action = self.ActionClass(self.decision)
assert action.should_hold_action() is False
AddonUser.objects.create(addon=self.rating.addon, user=self.rating.user)
assert action.should_hold_action() is False
self.make_addon_promoted(self.rating.addon, RECOMMENDED, approve_version=True)
assert action.should_hold_action() is False
self.rating.update(
reply_to=Rating.objects.create(
addon=self.rating.addon, user=user_factory(), body='original'
)
)
assert action.should_hold_action() is True
self.rating.update(deleted=self.rating.id)
assert action.should_hold_action() is False
def test_hold_action(self):
self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING)
action = self.ActionClass(self.decision)
activity = action.hold_action()
assert activity.log == amo.LOG.HELD_ACTION_DELETE_RATING
assert ActivityLog.objects.count() == 1
assert activity.arguments == [self.rating, self.policy, self.rating.addon]
assert activity.user == self.task_user
assert activity.details == {
'comments': self.decision.notes,
'cinder_action': DECISION_ACTIONS.AMO_DELETE_RATING,
}

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

@ -1270,7 +1270,6 @@ class TestCinderWebhook(TestCase):
process_mock.assert_called() process_mock.assert_called()
process_mock.assert_called_with( process_mock.assert_called_with(
decision_cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', decision_cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
decision_date=datetime(2023, 10, 12, 9, 8, 37, 4789),
decision_action=DECISION_ACTIONS.AMO_DISABLE_ADDON.value, decision_action=DECISION_ACTIONS.AMO_DISABLE_ADDON.value,
decision_notes='some notes', decision_notes='some notes',
policy_ids=['f73ad527-54ed-430c-86ff-80e15e2a352b'], policy_ids=['f73ad527-54ed-430c-86ff-80e15e2a352b'],
@ -1287,7 +1286,6 @@ class TestCinderWebhook(TestCase):
original_cinder_job = CinderJob.objects.get() original_cinder_job = CinderJob.objects.get()
original_cinder_job.update( original_cinder_job.update(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
date=datetime(2023, 10, 12, 9, 8, 37, 4789),
cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
appeal_job=CinderJob.objects.create( appeal_job=CinderJob.objects.create(
@ -1302,7 +1300,6 @@ class TestCinderWebhook(TestCase):
assert process_mock.call_count == 1 assert process_mock.call_count == 1
process_mock.assert_called_with( process_mock.assert_called_with(
decision_cinder_id='76e0006d-1a42-4ec7-9475-148bab1970f1', decision_cinder_id='76e0006d-1a42-4ec7-9475-148bab1970f1',
decision_date=datetime(2024, 4, 24, 17, 45, 32, 8810),
decision_action=DECISION_ACTIONS.AMO_APPROVE.value, decision_action=DECISION_ACTIONS.AMO_APPROVE.value,
decision_notes='still no!', decision_notes='still no!',
policy_ids=['1c5d711a-78b7-4fc2-bdef-9a33024f5e8b'], policy_ids=['1c5d711a-78b7-4fc2-bdef-9a33024f5e8b'],
@ -1325,7 +1322,7 @@ class TestCinderWebhook(TestCase):
original_cinder_job = CinderJob.objects.get() original_cinder_job = CinderJob.objects.get()
original_cinder_job.update( original_cinder_job.update(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
date=datetime(2023, 10, 12, 9, 8, 37, 4789), action_date=datetime(2023, 10, 12, 9, 8, 37, 4789),
cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
appeal_job=CinderJob.objects.create( appeal_job=CinderJob.objects.create(
@ -1340,7 +1337,6 @@ class TestCinderWebhook(TestCase):
assert process_mock.call_count == 1 assert process_mock.call_count == 1
process_mock.assert_called_with( process_mock.assert_called_with(
decision_cinder_id='4f18b22c-6078-4934-b395-6a2e01cadf63', decision_cinder_id='4f18b22c-6078-4934-b395-6a2e01cadf63',
decision_date=datetime(2024, 4, 24, 18, 19, 30, 274623),
decision_action=DECISION_ACTIONS.AMO_DISABLE_ADDON.value, decision_action=DECISION_ACTIONS.AMO_DISABLE_ADDON.value,
decision_notes="fine I'll disable it", decision_notes="fine I'll disable it",
policy_ids=[ policy_ids=[
@ -1359,7 +1355,7 @@ class TestCinderWebhook(TestCase):
original_cinder_job = CinderJob.objects.get() original_cinder_job = CinderJob.objects.get()
original_cinder_job.update( original_cinder_job.update(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
date=datetime(2023, 10, 12, 9, 8, 37, 4789), action_date=datetime(2023, 10, 12, 9, 8, 37, 4789),
cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
appeal_job=CinderJob.objects.create( appeal_job=CinderJob.objects.create(
@ -1386,7 +1382,7 @@ class TestCinderWebhook(TestCase):
original_cinder_job = CinderJob.objects.get() original_cinder_job = CinderJob.objects.get()
original_cinder_job.update( original_cinder_job.update(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
date=datetime(2023, 10, 12, 9, 8, 37, 4789), action_date=datetime(2023, 10, 12, 9, 8, 37, 4789),
cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
appeal_job=CinderJob.objects.create( appeal_job=CinderJob.objects.create(
@ -1413,7 +1409,7 @@ class TestCinderWebhook(TestCase):
original_cinder_job = CinderJob.objects.get() original_cinder_job = CinderJob.objects.get()
original_cinder_job.update( original_cinder_job.update(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
date=datetime(2023, 10, 12, 9, 8, 37, 4789), action_date=datetime(2023, 10, 12, 9, 8, 37, 4789),
cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
appeal_job=CinderJob.objects.create( appeal_job=CinderJob.objects.create(
@ -1448,7 +1444,7 @@ class TestCinderWebhook(TestCase):
original_cinder_job = CinderJob.objects.get() original_cinder_job = CinderJob.objects.get()
original_cinder_job.update( original_cinder_job.update(
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
date=datetime(2023, 10, 12, 9, 8, 37, 4789), action_date=datetime(2023, 10, 12, 9, 8, 37, 4789),
cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed', cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
appeal_job=CinderJob.objects.create( appeal_job=CinderJob.objects.create(
@ -2472,7 +2468,7 @@ class TestAppeal(TestCase):
decision=CinderDecision.objects.create( decision=CinderDecision.objects.create(
cinder_id='my-decision-id', cinder_id='my-decision-id',
action=DECISION_ACTIONS.AMO_APPROVE, action=DECISION_ACTIONS.AMO_APPROVE,
date=self.days_ago(1), action_date=self.days_ago(1),
addon=self.addon, addon=self.addon,
), ),
created=self.days_ago(2), created=self.days_ago(2),
@ -2599,7 +2595,7 @@ class TestAppeal(TestCase):
} }
def test_appeal_approval_anonymous_report_with_email_post_cant_be_appealed(self): def test_appeal_approval_anonymous_report_with_email_post_cant_be_appealed(self):
self.cinder_job.decision.update(date=self.days_ago(200)) self.cinder_job.decision.update(action_date=self.days_ago(200))
self.abuse_report.update(reporter_email='me@example.com') self.abuse_report.update(reporter_email='me@example.com')
response = self.client.get(self.reporter_appeal_url) response = self.client.get(self.reporter_appeal_url)
assert response.status_code == 200 assert response.status_code == 200
@ -2654,7 +2650,7 @@ class TestAppeal(TestCase):
assert self.appeal_mock.call_count == 0 assert self.appeal_mock.call_count == 0
def test_appeal_approval_logged_in_report_cant_be_appealed(self): def test_appeal_approval_logged_in_report_cant_be_appealed(self):
self.cinder_job.decision.update(date=self.days_ago(200)) self.cinder_job.decision.update(action_date=self.days_ago(200))
self.user = user_factory() self.user = user_factory()
self.abuse_report.update(reporter=self.user) self.abuse_report.update(reporter=self.user)
self.client.force_login(self.user) self.client.force_login(self.user)
@ -2720,6 +2716,7 @@ class TestAppeal(TestCase):
addon=self.addon, addon=self.addon,
action=DECISION_ACTIONS.AMO_DISABLE_ADDON, action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
cinder_id='some-decision-id', cinder_id='some-decision-id',
action_date=datetime.now(),
) )
author_appeal_url = reverse( author_appeal_url = reverse(
'abuse.appeal_author', kwargs={'decision_cinder_id': decision.cinder_id} 'abuse.appeal_author', kwargs={'decision_cinder_id': decision.cinder_id}

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

@ -51,11 +51,34 @@ class CinderAction:
f'{self.valid_targets}' f'{self.valid_targets}'
) )
def log_action(self, activity_log_action, *extra_args, extra_details=None):
return log_create(
activity_log_action,
self.target,
*(self.decision.policies.all()),
*extra_args,
details={
'comments': self.decision.notes,
'cinder_action': self.decision.action,
**(extra_details or {}),
},
)
def should_hold_action(self):
"""This should return false if the action should be processed immediately,
without further checks, and true if it should be held for further review."""
return False
def process_action(self): def process_action(self):
"""This method should return an activity log instance for the action, """This method should return an activity log instance for the action,
if available.""" if available."""
raise NotImplementedError raise NotImplementedError
def hold_action(self):
"""This method should take no action, but create an activity log instance with
appropriate details."""
pass
def get_owners(self): def get_owners(self):
"""No owner emails will be sent. Override to send owner emails""" """No owner emails will be sent. Override to send owner emails"""
return () return ()
@ -239,12 +262,30 @@ class CinderActionBanUser(CinderAction):
reporter_template_path = 'abuse/emails/reporter_takedown_user.txt' reporter_template_path = 'abuse/emails/reporter_takedown_user.txt'
reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt'
def should_hold_action(self):
return bool(
not self.target.banned
and (
self.target.is_staff # mozilla.com
or self.target.groups_list # has any permissions
# owns a high profile add-on
or any(
addon.promoted_group().high_profile
for addon in self.target.addons.all()
)
)
)
def process_action(self): def process_action(self):
if not self.target.banned: if not self.target.banned:
UserProfile.objects.filter( UserProfile.objects.filter(
pk=self.target.pk pk=self.target.pk
).ban_and_disable_related_content() ).ban_and_disable_related_content(skip_activity_log=True)
return None return self.log_action(amo.LOG.ADMIN_USER_BANNED)
def hold_action(self):
if not self.target.banned:
return self.log_action(amo.LOG.HELD_ACTION_ADMIN_USER_BANNED)
def get_owners(self): def get_owners(self):
return [self.target] return [self.target]
@ -256,10 +297,22 @@ class CinderActionDisableAddon(CinderAction):
reporter_template_path = 'abuse/emails/reporter_takedown_addon.txt' reporter_template_path = 'abuse/emails/reporter_takedown_addon.txt'
reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt'
def should_hold_action(self):
return bool(
self.target.status != amo.STATUS_DISABLED
# is a high profile add-on
and self.target.promoted_group().high_profile
)
def process_action(self): def process_action(self):
if self.target.status != amo.STATUS_DISABLED: if self.target.status != amo.STATUS_DISABLED:
self.target.force_disable(skip_activity_log=True) self.target.force_disable(skip_activity_log=True)
return log_create(amo.LOG.FORCE_DISABLE, self.target) return self.log_action(amo.LOG.FORCE_DISABLE)
return None
def hold_action(self):
if self.target.status != amo.STATUS_DISABLED:
return self.log_action(amo.LOG.HELD_ACTION_FORCE_DISABLE)
return None return None
def get_owners(self): def get_owners(self):
@ -269,10 +322,19 @@ class CinderActionDisableAddon(CinderAction):
class CinderActionRejectVersion(CinderActionDisableAddon): class CinderActionRejectVersion(CinderActionDisableAddon):
description = 'Add-on version(s) have been rejected' description = 'Add-on version(s) have been rejected'
def should_hold_action(self):
# This action should only be used by reviewer tools, not cinder webhook
# eventually, if add-on becomes non-public do as disable
raise NotImplementedError
def process_action(self): def process_action(self):
# This action should only be used by reviewer tools, not cinder webhook # This action should only be used by reviewer tools, not cinder webhook
raise NotImplementedError raise NotImplementedError
def hold_action(self):
# This action should only be used by reviewer tools, not cinder webhook
raise NotImplementedError
class CinderActionRejectVersionDelayed(CinderActionRejectVersion): class CinderActionRejectVersionDelayed(CinderActionRejectVersion):
description = 'Add-on version(s) will be rejected' description = 'Add-on version(s) will be rejected'
@ -295,10 +357,21 @@ class CinderActionDeleteCollection(CinderAction):
reporter_template_path = 'abuse/emails/reporter_takedown_collection.txt' reporter_template_path = 'abuse/emails/reporter_takedown_collection.txt'
reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt'
def should_hold_action(self):
return (
# Mozilla-owned collection
not self.target.deleted and self.target.author_id == settings.TASK_USER_ID
)
def process_action(self): def process_action(self):
if not self.target.deleted: if not self.target.deleted:
self.target.delete(clear_slug=False) self.target.delete(clear_slug=False)
return log_create(amo.LOG.COLLECTION_DELETED, self.target) return self.log_action(amo.LOG.COLLECTION_DELETED)
return None
def hold_action(self):
if not self.target.deleted:
return self.log_action(amo.LOG.HELD_ACTION_COLLECTION_DELETED)
return None return None
def get_owners(self): def get_owners(self):
@ -311,9 +384,32 @@ class CinderActionDeleteRating(CinderAction):
reporter_template_path = 'abuse/emails/reporter_takedown_rating.txt' reporter_template_path = 'abuse/emails/reporter_takedown_rating.txt'
reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt' reporter_appeal_template_path = 'abuse/emails/reporter_appeal_takedown.txt'
def should_hold_action(self):
# Developer reply in recommended or partner extensions
return bool(
not self.target.deleted
and self.target.reply_to
and self.target.addon.promoted_group().high_profile_rating
)
def process_action(self): def process_action(self):
if not self.target.deleted: if not self.target.deleted:
self.target.delete(clear_flags=False) self.target.delete(skip_activity_log=True, clear_flags=False)
return self.log_action(
amo.LOG.DELETE_RATING,
self.target.addon,
extra_details={
'body': str(self.target.body),
'addon_id': self.target.addon.pk,
'addon_title': str(self.target.addon.name),
'is_flagged': self.target.ratingflag_set.exists(),
},
)
return None
def hold_action(self):
if not self.target.deleted:
return self.log_action(amo.LOG.HELD_ACTION_DELETE_RATING, self.target.addon)
return None return None
def get_owners(self): def get_owners(self):

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

@ -1,6 +1,5 @@
import hashlib import hashlib
import hmac import hmac
from datetime import datetime, timezone
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@ -169,18 +168,6 @@ class CinderInboundPermission:
return hmac.compare_digest(header, digest) return hmac.compare_digest(header, digest)
def process_datestamp(date_string):
try:
return (
datetime.fromisoformat(date_string.replace(' ', ''))
.astimezone(timezone.utc)
.replace(tzinfo=None)
)
except ValueError:
log.warn('Invalid timestamp from cinder webhook %s', date_string)
return datetime.now()
def filter_enforcement_actions(enforcement_actions, cinder_job): def filter_enforcement_actions(enforcement_actions, cinder_job):
target = cinder_job.target target = cinder_job.target
if not target: if not target:
@ -253,7 +240,6 @@ def cinder_webhook(request):
cinder_job.process_decision( cinder_job.process_decision(
decision_cinder_id=source.get('decision', {}).get('id'), decision_cinder_id=source.get('decision', {}).get('id'),
decision_date=process_datestamp(payload.get('timestamp')),
decision_action=enforcement_actions[0], decision_action=enforcement_actions[0],
decision_notes=payload.get('notes') or '', decision_notes=payload.get('notes') or '',
policy_ids=policy_ids, policy_ids=policy_ids,

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

@ -1059,6 +1059,39 @@ class DENY_APPEAL_JOB(_LOG):
reviewer_review_action = True reviewer_review_action = True
class HELD_ACTION_ADMIN_USER_BANNED(_LOG):
id = 193
format = _('User {user} ban action held for further review.')
short = 'Held user ban'
admin_event = True
class HELD_ACTION_DELETE_RATING(_LOG):
"""Requires rating.id and add-on objects."""
id = 194
action_class = 'review'
format = _('Review {rating} for {addon} delete held for further review.')
reviewer_format = 'Held {user_responsible}s delete {rating} for {addon}'
admin_event = True
class HELD_ACTION_COLLECTION_DELETED(_LOG):
id = 195
format = _('Collection {collection} deletion held for further review')
admin_event = True
class HELD_ACTION_FORCE_DISABLE(_LOG):
id = 196
reviewer_review_action = True
format = _('{addon} force-disable held for further review')
reviewer_format = 'Held {addon} force-disable by {user_responsible}.'
admin_format = reviewer_format
short = 'Held force disable'
admin_event = True
LOGS = [x for x in vars().values() if isclass(x) and issubclass(x, _LOG) and x != _LOG] LOGS = [x for x in vars().values() if isclass(x) and issubclass(x, _LOG) and x != _LOG]
# Make sure there's no duplicate IDs. # Make sure there's no duplicate IDs.
assert len(LOGS) == len({log.id for log in LOGS}) assert len(LOGS) == len({log.id for log in LOGS})

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

@ -18,10 +18,12 @@ _PromotedSuperClass = namedtuple(
'admin_review', 'admin_review',
'badged', # See BADGE_CATEGORIES in frontend too: both need changing 'badged', # See BADGE_CATEGORIES in frontend too: both need changing
'autograph_signing_states', 'autograph_signing_states',
'can_primary_hero', 'can_primary_hero', # can be added to a primary hero shelf
'immediate_approval', 'immediate_approval', # will addon be auto-approved once added
'flag_for_human_review', 'flag_for_human_review', # will be add-on be flagged for another review
'can_be_compatible_with_all_fenix_versions', # If addon is promoted for Android 'can_be_compatible_with_all_fenix_versions', # If addon is promoted for Android
'high_profile', # the add-on is considered high-profile for review purposes
'high_profile_rating', # developer replies are considered high-profile
], ],
defaults=( defaults=(
# "Since fields with a default value must come after any fields without # "Since fields with a default value must come after any fields without
@ -33,10 +35,12 @@ _PromotedSuperClass = namedtuple(
False, # admin_review False, # admin_review
False, # badged False, # badged
{}, # autograph_signing_states - should be a dict of App.short: state {}, # autograph_signing_states - should be a dict of App.short: state
False, # can_primary_hero - can be added to a primary hero shelf False, # can_primary_hero
False, # immediate_approval - will addon be auto-approved once added False, # immediate_approval
False, # flag_for_human_review - will be add-on be flagged for another review False, # flag_for_human_review
False, # can_be_compatible_with_all_fenix_versions False, # can_be_compatible_with_all_fenix_versions
False, # high_profile
False, # high_profile_rating
), ),
) )
@ -67,6 +71,8 @@ RECOMMENDED = PromotedClass(
}, },
can_primary_hero=True, can_primary_hero=True,
can_be_compatible_with_all_fenix_versions=True, can_be_compatible_with_all_fenix_versions=True,
high_profile=True,
high_profile_rating=True,
) )
# Obsolete, never used in production, only there to prevent us from re-using # Obsolete, never used in production, only there to prevent us from re-using
@ -89,6 +95,8 @@ LINE = PromotedClass(
}, },
can_primary_hero=True, can_primary_hero=True,
can_be_compatible_with_all_fenix_versions=True, can_be_compatible_with_all_fenix_versions=True,
high_profile=True,
high_profile_rating=True,
) )
SPOTLIGHT = PromotedClass( SPOTLIGHT = PromotedClass(
@ -99,6 +107,7 @@ SPOTLIGHT = PromotedClass(
admin_review=True, admin_review=True,
can_primary_hero=True, can_primary_hero=True,
immediate_approval=True, immediate_approval=True,
high_profile=True,
) )
STRATEGIC = PromotedClass( STRATEGIC = PromotedClass(
@ -115,6 +124,7 @@ NOTABLE = PromotedClass(
listed_pre_review=True, listed_pre_review=True,
unlisted_pre_review=True, unlisted_pre_review=True,
flag_for_human_review=True, flag_for_human_review=True,
high_profile=True,
) )

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

@ -111,7 +111,7 @@ class UserEmailBoundField(forms.boundfield.BoundField):
class UserQuerySet(BaseQuerySet): class UserQuerySet(BaseQuerySet):
def ban_and_disable_related_content(self): def ban_and_disable_related_content(self, *, skip_activity_log=False):
"""Admin method to ban multiple users and disable the content they """Admin method to ban multiple users and disable the content they
produced. produced.
@ -222,7 +222,8 @@ class UserQuerySet(BaseQuerySet):
ratings_qs.delete() ratings_qs.delete()
# And then ban the users. # And then ban the users.
for user in users: for user in users:
activity.log_create(amo.LOG.ADMIN_USER_BANNED, user) if not skip_activity_log:
activity.log_create(amo.LOG.ADMIN_USER_BANNED, user)
log.info( log.info(
'User (%s: <%s>) is being banned.', 'User (%s: <%s>) is being banned.',
user, user,