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:
Родитель
3a7d24badb
Коммит
8b100c94ae
|
@ -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.models import Exists, OuterRef, Q
|
||||
from django.db.transaction import atomic
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from olympia import amo
|
||||
|
@ -278,14 +277,13 @@ class CinderJob(ModelBase):
|
|||
self,
|
||||
*,
|
||||
decision_cinder_id,
|
||||
decision_date,
|
||||
decision_action,
|
||||
decision_notes,
|
||||
policy_ids,
|
||||
):
|
||||
"""This is called for cinder 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
|
||||
abuse_report_or_decision = (
|
||||
self.appealed_decisions.first() or self.abusereport_set.first()
|
||||
|
@ -301,7 +299,6 @@ class CinderJob(ModelBase):
|
|||
'rating': abuse_report_or_decision.rating,
|
||||
'collection': abuse_report_or_decision.collection,
|
||||
'user': abuse_report_or_decision.user,
|
||||
'date': decision_date,
|
||||
'cinder_id': decision_cinder_id,
|
||||
'action': decision_action,
|
||||
'notes': decision_notes[
|
||||
|
@ -313,14 +310,8 @@ class CinderJob(ModelBase):
|
|||
policies = CinderPolicy.objects.filter(
|
||||
uuid__in=policy_ids
|
||||
).without_parents_if_their_children_are_present()
|
||||
self.decision.policies.add(*policies)
|
||||
action_helper = self.decision.get_action_helper(
|
||||
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))
|
||||
cinder_decision.policies.add(*policies)
|
||||
cinder_decision.process_action(overridden_action)
|
||||
|
||||
def resolve_job(self, *, log_entry):
|
||||
"""This is called for reviewer tools originated decisions.
|
||||
|
@ -349,7 +340,7 @@ class CinderJob(ModelBase):
|
|||
appealed_action=getattr(self.appealed_decisions.first(), 'action', None),
|
||||
)
|
||||
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)
|
||||
self.pending_rejections.add(
|
||||
*VersionReviewerFlags.objects.filter(version__in=version_list)
|
||||
|
@ -920,7 +911,7 @@ class CinderPolicy(ModelBase):
|
|||
class CinderDecision(ModelBase):
|
||||
action = models.PositiveSmallIntegerField(choices=DECISION_ACTIONS.choices)
|
||||
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)
|
||||
policies = models.ManyToManyField(to='abuse.CinderPolicy')
|
||||
appeal_job = models.ForeignKey(
|
||||
|
@ -1022,7 +1013,7 @@ class CinderDecision(ModelBase):
|
|||
DECISION_ACTIONS.AMO_CLOSED_NO_ACTION: CinderActionAlreadyRemoved,
|
||||
}.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
|
||||
CinderActionClass = self.get_action_helper_class(self.action)
|
||||
skip_reporter_notify = False
|
||||
|
@ -1038,11 +1029,11 @@ class CinderDecision(ModelBase):
|
|||
CinderActionClass = CinderActionTargetAppealRemovalAffirmation
|
||||
# (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
|
||||
if self.action in DECISION_ACTIONS.APPROVING:
|
||||
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
|
||||
# to the target; but we don't want to notify the reporter again.
|
||||
skip_reporter_notify = True
|
||||
|
@ -1059,8 +1050,8 @@ class CinderDecision(ModelBase):
|
|||
"""
|
||||
now = datetime.now()
|
||||
base_criteria = (
|
||||
self.date
|
||||
and self.date >= now - timedelta(days=APPEAL_EXPIRATION_DAYS)
|
||||
self.action_date
|
||||
and self.action_date >= now - timedelta(days=APPEAL_EXPIRATION_DAYS)
|
||||
# Can never appeal an original decision that has been appealed and
|
||||
# for which we already have a new decision. In some cases the
|
||||
# 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 '
|
||||
'notify_reviewer_decision'
|
||||
)
|
||||
overriden_action = self.action
|
||||
overridden_action = self.action
|
||||
self.action = DECISION_ACTIONS.for_constant(
|
||||
log_entry.details['cinder_action']
|
||||
).value
|
||||
|
@ -1201,7 +1192,7 @@ class CinderDecision(ModelBase):
|
|||
'reasoning': self.notes,
|
||||
'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)
|
||||
):
|
||||
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)
|
||||
with atomic():
|
||||
self.cinder_id = decision_cinder_id
|
||||
self.action_date = datetime.now()
|
||||
self.save()
|
||||
self.policies.set(policies)
|
||||
|
||||
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):
|
||||
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):
|
||||
text = models.TextField(blank=False, help_text='The content of the appeal.')
|
||||
|
|
|
@ -31,6 +31,8 @@ from olympia.constants.abuse import (
|
|||
ILLEGAL_CATEGORIES,
|
||||
ILLEGAL_SUBCATEGORIES,
|
||||
)
|
||||
from olympia.constants.promoted import RECOMMENDED
|
||||
from olympia.core import set_user
|
||||
from olympia.ratings.models import Rating
|
||||
from olympia.reviewers.models import NeedsHumanReview
|
||||
from olympia.versions.models import Version, VersionReviewerFlags
|
||||
|
@ -1172,7 +1174,6 @@ class TestCinderJob(TestCase):
|
|||
cinder_job = CinderJob.objects.create(job_id='1234')
|
||||
target = user_factory()
|
||||
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_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))
|
||||
cinder_job.process_decision(
|
||||
decision_cinder_id='12345',
|
||||
decision_date=new_date,
|
||||
decision_action=DECISION_ACTIONS.AMO_BAN_USER.value,
|
||||
decision_notes='teh notes',
|
||||
policy_ids=['123-45', '678-90'],
|
||||
)
|
||||
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.notes == 'teh notes'
|
||||
assert cinder_job.decision.user == target
|
||||
|
@ -1202,7 +1201,6 @@ class TestCinderJob(TestCase):
|
|||
cinder_job = CinderJob.objects.create(job_id='1234')
|
||||
target = user_factory()
|
||||
AbuseReport.objects.create(user=target, cinder_job=cinder_job)
|
||||
new_date = datetime(2023, 1, 1)
|
||||
parent_policy = CinderPolicy.objects.create(
|
||||
uuid='678-90', name='bbb', text='BBB'
|
||||
)
|
||||
|
@ -1218,13 +1216,11 @@ class TestCinderJob(TestCase):
|
|||
action_mock.return_value = (True, None)
|
||||
cinder_job.process_decision(
|
||||
decision_cinder_id='12345',
|
||||
decision_date=new_date,
|
||||
decision_action=DECISION_ACTIONS.AMO_BAN_USER.value,
|
||||
decision_notes='teh notes',
|
||||
policy_ids=['123-45', '678-90'],
|
||||
)
|
||||
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.notes == 'teh notes'
|
||||
assert cinder_job.decision.user == target
|
||||
|
@ -1237,7 +1233,6 @@ class TestCinderJob(TestCase):
|
|||
cinder_job = CinderJob.objects.create(job_id='1234', target_addon=addon)
|
||||
report = AbuseReport.objects.create(guid=addon.guid, cinder_job=cinder_job)
|
||||
assert not cinder_job.resolvable_in_reviewer_tools
|
||||
new_date = datetime(2024, 1, 1)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
f'{settings.CINDER_SERVER_URL}create_report',
|
||||
|
@ -1247,7 +1242,6 @@ class TestCinderJob(TestCase):
|
|||
|
||||
cinder_job.process_decision(
|
||||
decision_cinder_id='12345',
|
||||
decision_date=new_date,
|
||||
decision_action=DECISION_ACTIONS.AMO_ESCALATE_ADDON,
|
||||
decision_notes='blah',
|
||||
policy_ids=[],
|
||||
|
@ -1325,7 +1319,7 @@ class TestCinderJob(TestCase):
|
|||
assert 'entity' not in request_body
|
||||
cinder_job.reload()
|
||||
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 len(mail.outbox) == (2 if expect_target_email else 1)
|
||||
assert mail.outbox[0].to == [abuse_report.reporter.email]
|
||||
|
@ -1399,7 +1393,7 @@ class TestCinderJob(TestCase):
|
|||
assert cinder_job.decision.action == (
|
||||
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 set(cinder_job.pending_rejections.all()) == set(
|
||||
VersionReviewerFlags.objects.filter(
|
||||
|
@ -1470,7 +1464,7 @@ class TestCinderJob(TestCase):
|
|||
assert 'entity' not in request_body
|
||||
appeal_job.reload()
|
||||
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 len(mail.outbox) == 1
|
||||
|
||||
|
@ -1602,7 +1596,7 @@ class TestCinderJob(TestCase):
|
|||
assert request_body['reasoning'] == 'some review text'
|
||||
cinder_job.reload()
|
||||
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 len(mail.outbox) == 2
|
||||
assert mail.outbox[0].to == [abuse_report.reporter.email]
|
||||
|
@ -1797,6 +1791,7 @@ class TestCinderDecisionCanBeAppealed(TestCase):
|
|||
cinder_id='fake_decision_id',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=self.addon,
|
||||
action_date=datetime.now(),
|
||||
)
|
||||
|
||||
def test_appealed_decision_already_made(self):
|
||||
|
@ -1937,6 +1932,7 @@ class TestCinderDecisionCanBeAppealed(TestCase):
|
|||
cinder_id='fake_appeal_decision_id',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=self.addon,
|
||||
action_date=datetime.now(),
|
||||
),
|
||||
)
|
||||
report = AbuseReport.objects.create(
|
||||
|
@ -1970,7 +1966,22 @@ class TestCinderDecisionCanBeAppealed(TestCase):
|
|||
assert self.decision.can_be_appealed(
|
||||
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(
|
||||
is_reporter=True, abuse_report=initial_report
|
||||
)
|
||||
|
@ -2021,6 +2032,7 @@ class TestCinderDecisionCanBeAppealed(TestCase):
|
|||
cinder_id='fake_appeal_decision_id',
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
addon=self.addon,
|
||||
action_date=datetime.now(),
|
||||
),
|
||||
)
|
||||
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-appeals-review', active=True)
|
||||
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):
|
||||
decision = CinderDecision()
|
||||
assert decision.get_reference_id() == 'NoClass#None'
|
||||
|
@ -2292,7 +2310,7 @@ class TestCinderDecision(TestCase):
|
|||
}
|
||||
)
|
||||
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.decision == decision
|
||||
|
@ -2321,7 +2339,7 @@ class TestCinderDecision(TestCase):
|
|||
}
|
||||
)
|
||||
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_appeal_template_path is None
|
||||
|
@ -2329,7 +2347,6 @@ class TestCinderDecision(TestCase):
|
|||
assert ActionClass.reporter_appeal_template_path is not None
|
||||
|
||||
def _test_appeal_as_target(self, *, resolvable_in_reviewer_tools):
|
||||
user_factory(id=settings.TASK_USER_ID)
|
||||
addon = addon_factory(
|
||||
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,
|
||||
decision=CinderDecision.objects.create(
|
||||
cinder_id='4815162342-lost',
|
||||
date=self.days_ago(179),
|
||||
action_date=self.days_ago(179),
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
addon=addon,
|
||||
),
|
||||
|
@ -2405,7 +2422,7 @@ class TestCinderDecision(TestCase):
|
|||
cinder_job=CinderJob.objects.create(
|
||||
decision=CinderDecision.objects.create(
|
||||
cinder_id='4815162342-lost',
|
||||
date=self.days_ago(179),
|
||||
action_date=self.days_ago(179),
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
addon=addon,
|
||||
),
|
||||
|
@ -2443,7 +2460,7 @@ class TestCinderDecision(TestCase):
|
|||
cinder_job=CinderJob.objects.create(
|
||||
decision=CinderDecision.objects.create(
|
||||
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
|
||||
# be possible but we want to make sure this is handled
|
||||
# explicitly.
|
||||
|
@ -2487,7 +2504,7 @@ class TestCinderDecision(TestCase):
|
|||
cinder_job=CinderJob.objects.create(
|
||||
decision=CinderDecision.objects.create(
|
||||
cinder_id='4815162342-lost',
|
||||
date=self.days_ago(179),
|
||||
action_date=self.days_ago(179),
|
||||
action=DECISION_ACTIONS.AMO_BAN_USER,
|
||||
user=target,
|
||||
)
|
||||
|
@ -2528,7 +2545,7 @@ class TestCinderDecision(TestCase):
|
|||
target_addon=addon,
|
||||
decision=CinderDecision.objects.create(
|
||||
cinder_id='4815162342-lost',
|
||||
date=self.days_ago(179),
|
||||
action_date=self.days_ago(179),
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=addon,
|
||||
),
|
||||
|
@ -2572,7 +2589,7 @@ class TestCinderDecision(TestCase):
|
|||
target_addon=addon,
|
||||
decision=CinderDecision.objects.create(
|
||||
cinder_id='4815162342-lost',
|
||||
date=self.days_ago(179),
|
||||
action_date=self.days_ago(179),
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=addon,
|
||||
),
|
||||
|
@ -2615,7 +2632,7 @@ class TestCinderDecision(TestCase):
|
|||
cinder_job = CinderJob.objects.create(
|
||||
decision=CinderDecision.objects.create(
|
||||
cinder_id='4815162342-lost',
|
||||
date=self.days_ago(179),
|
||||
action_date=self.days_ago(179),
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=addon_factory(),
|
||||
)
|
||||
|
@ -2638,7 +2655,7 @@ class TestCinderDecision(TestCase):
|
|||
cinder_job = CinderJob.objects.create(
|
||||
decision=CinderDecision.objects.create(
|
||||
cinder_id='4815162342-lost',
|
||||
date=self.days_ago(179),
|
||||
action_date=self.days_ago(179),
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=addon,
|
||||
)
|
||||
|
@ -2715,7 +2732,7 @@ class TestCinderDecision(TestCase):
|
|||
assert request_body['enforcement_actions_slugs'] == [
|
||||
cinder_action.api_value
|
||||
]
|
||||
self.assertCloseToNow(decision.date)
|
||||
self.assertCloseToNow(decision.action_date)
|
||||
assert list(decision.policies.all()) == policies
|
||||
assert CinderDecision.objects.count() == 1
|
||||
assert decision.id
|
||||
|
@ -2730,7 +2747,7 @@ class TestCinderDecision(TestCase):
|
|||
assert request_body['enforcement_actions_slugs'] == [
|
||||
cinder_action.api_value
|
||||
]
|
||||
self.assertCloseToNow(decision.date)
|
||||
self.assertCloseToNow(decision.action_date)
|
||||
assert list(decision.policies.all()) == policies
|
||||
assert CinderDecision.objects.count() == 1
|
||||
assert decision.id
|
||||
|
@ -2995,6 +3012,130 @@ class TestCinderDecision(TestCase):
|
|||
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.parametrize(
|
||||
|
|
|
@ -434,6 +434,7 @@ def test_addon_appeal_to_cinder_reporter(statsd_incr_mock):
|
|||
cinder_id='4815162342-abc',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=addon,
|
||||
action_date=datetime.now(),
|
||||
)
|
||||
)
|
||||
abuse_report = AbuseReport.objects.create(
|
||||
|
@ -495,6 +496,7 @@ def test_addon_appeal_to_cinder_reporter_exception(statsd_incr_mock):
|
|||
cinder_id='4815162342-abc',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=addon,
|
||||
action_date=datetime.now(),
|
||||
)
|
||||
)
|
||||
abuse_report = AbuseReport.objects.create(
|
||||
|
@ -536,6 +538,7 @@ def test_addon_appeal_to_cinder_authenticated_reporter():
|
|||
cinder_id='4815162342-abc',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
addon=addon,
|
||||
action_date=datetime.now(),
|
||||
)
|
||||
)
|
||||
abuse_report = AbuseReport.objects.create(
|
||||
|
@ -593,6 +596,7 @@ def test_addon_appeal_to_cinder_authenticated_author():
|
|||
cinder_id='4815162342-abc',
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
addon=addon,
|
||||
action_date=datetime.now(),
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.urls import reverse
|
||||
|
@ -6,9 +8,10 @@ from waffle.testutils import override_switch
|
|||
|
||||
from olympia import amo
|
||||
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.constants.abuse import DECISION_ACTIONS
|
||||
from olympia.constants.promoted import RECOMMENDED
|
||||
from olympia.core import set_user
|
||||
from olympia.ratings.models import Rating
|
||||
|
||||
|
@ -37,22 +40,22 @@ class BaseTestCinderAction:
|
|||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
notes="extra note's",
|
||||
addon=addon,
|
||||
action_date=datetime.now(),
|
||||
)
|
||||
self.cinder_job = CinderJob.objects.create(
|
||||
job_id='1234', decision=self.decision
|
||||
)
|
||||
self.decision.policies.add(
|
||||
CinderPolicy.objects.create(
|
||||
uuid='1234',
|
||||
name='Bad policy',
|
||||
text='This is bad thing',
|
||||
parent=CinderPolicy.objects.create(
|
||||
uuid='p4r3nt',
|
||||
name='Parent Policy',
|
||||
text='Parent policy text',
|
||||
),
|
||||
)
|
||||
self.policy = CinderPolicy.objects.create(
|
||||
uuid='1234',
|
||||
name='Bad policy',
|
||||
text='This is bad thing',
|
||||
parent=CinderPolicy.objects.create(
|
||||
uuid='p4r3nt',
|
||||
name='Parent Policy',
|
||||
text='Parent policy text',
|
||||
),
|
||||
)
|
||||
self.decision.policies.add(self.policy)
|
||||
self.abuse_report_no_auth = AbuseReport.objects.create(
|
||||
reason=AbuseReport.REASONS.HATEFUL_VIOLENT_DECEPTIVE,
|
||||
guid=addon.guid,
|
||||
|
@ -332,14 +335,18 @@ class TestCinderActionUser(BaseTestCinderAction, TestCase):
|
|||
def _test_ban_user(self):
|
||||
self.decision.update(action=DECISION_ACTIONS.AMO_BAN_USER)
|
||||
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.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
|
||||
|
||||
self.cinder_job.notify_reporters(action)
|
||||
|
@ -420,6 +427,43 @@ class TestCinderActionUser(BaseTestCinderAction, TestCase):
|
|||
action.notify_owners()
|
||||
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-appeals-review', active=True)
|
||||
|
@ -442,7 +486,7 @@ class TestCinderActionAddon(BaseTestCinderAction, TestCase):
|
|||
assert activity.log == amo.LOG.FORCE_DISABLE
|
||||
assert self.addon.reload().status == amo.STATUS_DISABLED
|
||||
assert ActivityLog.objects.count() == 1
|
||||
assert activity.arguments == [self.addon]
|
||||
assert activity.arguments == [self.addon, self.policy]
|
||||
assert activity.user == self.task_user
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
@ -762,6 +806,30 @@ class TestCinderActionAddon(BaseTestCinderAction, TestCase):
|
|||
)
|
||||
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):
|
||||
ActionClass = CinderActionDeleteCollection
|
||||
|
@ -788,7 +856,7 @@ class TestCinderActionCollection(BaseTestCinderAction, TestCase):
|
|||
assert ActivityLog.objects.count() == 1
|
||||
activity = ActivityLog.objects.get(action=amo.LOG.COLLECTION_DELETED.id)
|
||||
assert activity == log_entry
|
||||
assert activity.arguments == [self.collection]
|
||||
assert activity.arguments == [self.collection, self.policy]
|
||||
assert activity.user == self.task_user
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
@ -870,6 +938,30 @@ class TestCinderActionCollection(BaseTestCinderAction, TestCase):
|
|||
action.notify_owners()
|
||||
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):
|
||||
ActionClass = CinderActionDeleteRating
|
||||
|
@ -887,13 +979,21 @@ class TestCinderActionRating(BaseTestCinderAction, TestCase):
|
|||
def _test_delete_rating(self):
|
||||
self.decision.update(action=DECISION_ACTIONS.AMO_DELETE_RATING)
|
||||
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 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
|
||||
|
||||
self.cinder_job.notify_reporters(action)
|
||||
|
@ -977,3 +1077,35 @@ class TestCinderActionRating(BaseTestCinderAction, TestCase):
|
|||
self._test_owner_affirmation_email(
|
||||
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_with(
|
||||
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_notes='some notes',
|
||||
policy_ids=['f73ad527-54ed-430c-86ff-80e15e2a352b'],
|
||||
|
@ -1287,7 +1286,6 @@ class TestCinderWebhook(TestCase):
|
|||
original_cinder_job = CinderJob.objects.get()
|
||||
original_cinder_job.update(
|
||||
decision=CinderDecision.objects.create(
|
||||
date=datetime(2023, 10, 12, 9, 8, 37, 4789),
|
||||
cinder_id='d1f01fae-3bce-41d5-af8a-e0b4b5ceaaed',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
appeal_job=CinderJob.objects.create(
|
||||
|
@ -1302,7 +1300,6 @@ class TestCinderWebhook(TestCase):
|
|||
assert process_mock.call_count == 1
|
||||
process_mock.assert_called_with(
|
||||
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_notes='still no!',
|
||||
policy_ids=['1c5d711a-78b7-4fc2-bdef-9a33024f5e8b'],
|
||||
|
@ -1325,7 +1322,7 @@ class TestCinderWebhook(TestCase):
|
|||
original_cinder_job = CinderJob.objects.get()
|
||||
original_cinder_job.update(
|
||||
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',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
appeal_job=CinderJob.objects.create(
|
||||
|
@ -1340,7 +1337,6 @@ class TestCinderWebhook(TestCase):
|
|||
assert process_mock.call_count == 1
|
||||
process_mock.assert_called_with(
|
||||
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_notes="fine I'll disable it",
|
||||
policy_ids=[
|
||||
|
@ -1359,7 +1355,7 @@ class TestCinderWebhook(TestCase):
|
|||
original_cinder_job = CinderJob.objects.get()
|
||||
original_cinder_job.update(
|
||||
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',
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
appeal_job=CinderJob.objects.create(
|
||||
|
@ -1386,7 +1382,7 @@ class TestCinderWebhook(TestCase):
|
|||
original_cinder_job = CinderJob.objects.get()
|
||||
original_cinder_job.update(
|
||||
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',
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
appeal_job=CinderJob.objects.create(
|
||||
|
@ -1413,7 +1409,7 @@ class TestCinderWebhook(TestCase):
|
|||
original_cinder_job = CinderJob.objects.get()
|
||||
original_cinder_job.update(
|
||||
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',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
appeal_job=CinderJob.objects.create(
|
||||
|
@ -1448,7 +1444,7 @@ class TestCinderWebhook(TestCase):
|
|||
original_cinder_job = CinderJob.objects.get()
|
||||
original_cinder_job.update(
|
||||
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',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
appeal_job=CinderJob.objects.create(
|
||||
|
@ -2472,7 +2468,7 @@ class TestAppeal(TestCase):
|
|||
decision=CinderDecision.objects.create(
|
||||
cinder_id='my-decision-id',
|
||||
action=DECISION_ACTIONS.AMO_APPROVE,
|
||||
date=self.days_ago(1),
|
||||
action_date=self.days_ago(1),
|
||||
addon=self.addon,
|
||||
),
|
||||
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):
|
||||
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')
|
||||
response = self.client.get(self.reporter_appeal_url)
|
||||
assert response.status_code == 200
|
||||
|
@ -2654,7 +2650,7 @@ class TestAppeal(TestCase):
|
|||
assert self.appeal_mock.call_count == 0
|
||||
|
||||
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.abuse_report.update(reporter=self.user)
|
||||
self.client.force_login(self.user)
|
||||
|
@ -2720,6 +2716,7 @@ class TestAppeal(TestCase):
|
|||
addon=self.addon,
|
||||
action=DECISION_ACTIONS.AMO_DISABLE_ADDON,
|
||||
cinder_id='some-decision-id',
|
||||
action_date=datetime.now(),
|
||||
)
|
||||
author_appeal_url = reverse(
|
||||
'abuse.appeal_author', kwargs={'decision_cinder_id': decision.cinder_id}
|
||||
|
|
|
@ -51,11 +51,34 @@ class CinderAction:
|
|||
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):
|
||||
"""This method should return an activity log instance for the action,
|
||||
if available."""
|
||||
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):
|
||||
"""No owner emails will be sent. Override to send owner emails"""
|
||||
return ()
|
||||
|
@ -239,12 +262,30 @@ class CinderActionBanUser(CinderAction):
|
|||
reporter_template_path = 'abuse/emails/reporter_takedown_user.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):
|
||||
if not self.target.banned:
|
||||
UserProfile.objects.filter(
|
||||
pk=self.target.pk
|
||||
).ban_and_disable_related_content()
|
||||
return None
|
||||
).ban_and_disable_related_content(skip_activity_log=True)
|
||||
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):
|
||||
return [self.target]
|
||||
|
@ -256,10 +297,22 @@ class CinderActionDisableAddon(CinderAction):
|
|||
reporter_template_path = 'abuse/emails/reporter_takedown_addon.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):
|
||||
if self.target.status != amo.STATUS_DISABLED:
|
||||
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
|
||||
|
||||
def get_owners(self):
|
||||
|
@ -269,10 +322,19 @@ class CinderActionDisableAddon(CinderAction):
|
|||
class CinderActionRejectVersion(CinderActionDisableAddon):
|
||||
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):
|
||||
# This action should only be used by reviewer tools, not cinder webhook
|
||||
raise NotImplementedError
|
||||
|
||||
def hold_action(self):
|
||||
# This action should only be used by reviewer tools, not cinder webhook
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CinderActionRejectVersionDelayed(CinderActionRejectVersion):
|
||||
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_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):
|
||||
if not self.target.deleted:
|
||||
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
|
||||
|
||||
def get_owners(self):
|
||||
|
@ -311,9 +384,32 @@ class CinderActionDeleteRating(CinderAction):
|
|||
reporter_template_path = 'abuse/emails/reporter_takedown_rating.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):
|
||||
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
|
||||
|
||||
def get_owners(self):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -169,18 +168,6 @@ class CinderInboundPermission:
|
|||
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):
|
||||
target = cinder_job.target
|
||||
if not target:
|
||||
|
@ -253,7 +240,6 @@ def cinder_webhook(request):
|
|||
|
||||
cinder_job.process_decision(
|
||||
decision_cinder_id=source.get('decision', {}).get('id'),
|
||||
decision_date=process_datestamp(payload.get('timestamp')),
|
||||
decision_action=enforcement_actions[0],
|
||||
decision_notes=payload.get('notes') or '',
|
||||
policy_ids=policy_ids,
|
||||
|
|
|
@ -1059,6 +1059,39 @@ class DENY_APPEAL_JOB(_LOG):
|
|||
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]
|
||||
# Make sure there's no duplicate IDs.
|
||||
assert len(LOGS) == len({log.id for log in LOGS})
|
||||
|
|
|
@ -18,10 +18,12 @@ _PromotedSuperClass = namedtuple(
|
|||
'admin_review',
|
||||
'badged', # See BADGE_CATEGORIES in frontend too: both need changing
|
||||
'autograph_signing_states',
|
||||
'can_primary_hero',
|
||||
'immediate_approval',
|
||||
'flag_for_human_review',
|
||||
'can_primary_hero', # can be added to a primary hero shelf
|
||||
'immediate_approval', # will addon be auto-approved once added
|
||||
'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
|
||||
'high_profile', # the add-on is considered high-profile for review purposes
|
||||
'high_profile_rating', # developer replies are considered high-profile
|
||||
],
|
||||
defaults=(
|
||||
# "Since fields with a default value must come after any fields without
|
||||
|
@ -33,10 +35,12 @@ _PromotedSuperClass = namedtuple(
|
|||
False, # admin_review
|
||||
False, # badged
|
||||
{}, # autograph_signing_states - should be a dict of App.short: state
|
||||
False, # can_primary_hero - can be added to a primary hero shelf
|
||||
False, # immediate_approval - will addon be auto-approved once added
|
||||
False, # flag_for_human_review - will be add-on be flagged for another review
|
||||
False, # can_primary_hero
|
||||
False, # immediate_approval
|
||||
False, # flag_for_human_review
|
||||
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_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
|
||||
|
@ -89,6 +95,8 @@ LINE = PromotedClass(
|
|||
},
|
||||
can_primary_hero=True,
|
||||
can_be_compatible_with_all_fenix_versions=True,
|
||||
high_profile=True,
|
||||
high_profile_rating=True,
|
||||
)
|
||||
|
||||
SPOTLIGHT = PromotedClass(
|
||||
|
@ -99,6 +107,7 @@ SPOTLIGHT = PromotedClass(
|
|||
admin_review=True,
|
||||
can_primary_hero=True,
|
||||
immediate_approval=True,
|
||||
high_profile=True,
|
||||
)
|
||||
|
||||
STRATEGIC = PromotedClass(
|
||||
|
@ -115,6 +124,7 @@ NOTABLE = PromotedClass(
|
|||
listed_pre_review=True,
|
||||
unlisted_pre_review=True,
|
||||
flag_for_human_review=True,
|
||||
high_profile=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ class UserEmailBoundField(forms.boundfield.BoundField):
|
|||
|
||||
|
||||
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
|
||||
produced.
|
||||
|
||||
|
@ -222,7 +222,8 @@ class UserQuerySet(BaseQuerySet):
|
|||
ratings_qs.delete()
|
||||
# And then ban the 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(
|
||||
'User (%s: <%s>) is being banned.',
|
||||
user,
|
||||
|
|
Загрузка…
Ссылка в новой задаче