Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/olympia/abuse/actions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import random
from collections import defaultdict
from datetime import datetime

from django.conf import settings
Expand All @@ -18,6 +19,7 @@
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.utils import send_mail
from olympia.bandwagon.models import Collection
from olympia.constants.abuse import DECISION_ACTIONS
from olympia.files.models import File
from olympia.ratings.models import Rating
from olympia.users.models import UserProfile
Expand Down Expand Up @@ -624,3 +626,27 @@ class ContentActionAlreadyRemoved(AnyTargetMixin, NoActionMixin, ContentAction):

class ContentActionNotImplemented(NoActionMixin, ContentAction):
pass


CONTENT_ACTION_FROM_DECISION_ACTION = defaultdict(
lambda: ContentActionNotImplemented,
{
DECISION_ACTIONS.AMO_BAN_USER: ContentActionBanUser,
DECISION_ACTIONS.AMO_DISABLE_ADDON: ContentActionDisableAddon,
DECISION_ACTIONS.AMO_REJECT_VERSION_ADDON: ContentActionRejectVersion,
DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON: (
ContentActionRejectVersionDelayed
),
DECISION_ACTIONS.AMO_ESCALATE_ADDON: ContentActionForwardToReviewers,
DECISION_ACTIONS.AMO_DELETE_COLLECTION: ContentActionDeleteCollection,
DECISION_ACTIONS.AMO_DELETE_RATING: ContentActionDeleteRating,
DECISION_ACTIONS.AMO_APPROVE: ContentActionApproveNoAction,
DECISION_ACTIONS.AMO_APPROVE_VERSION: ContentActionApproveInitialDecision,
DECISION_ACTIONS.AMO_IGNORE: ContentActionIgnore,
DECISION_ACTIONS.AMO_CLOSED_NO_ACTION: ContentActionAlreadyRemoved,
DECISION_ACTIONS.AMO_LEGAL_FORWARD: ContentActionForwardToLegal,
DECISION_ACTIONS.AMO_CHANGE_PENDING_REJECTION_DATE: (
ContentActionChangePendingRejectionDate
),
},
)
7 changes: 2 additions & 5 deletions src/olympia/abuse/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,6 @@ class CinderPolicyAdmin(AMOModelAdmin):
'text',
'expose_in_reviewer_tools',
'present_in_cinder',
'default_cinder_action',
)
list_display = (
'uuid',
Expand All @@ -383,12 +382,10 @@ class CinderPolicyAdmin(AMOModelAdmin):
'linked_review_reasons',
'expose_in_reviewer_tools',
'present_in_cinder',
'default_cinder_action',
'enforcement_actions',
'text',
)
readonly_fields = tuple(
set(fields) - {'expose_in_reviewer_tools', 'default_cinder_action'}
)
readonly_fields = tuple(set(fields) - {'expose_in_reviewer_tools'})
ordering = ('parent__name', 'name')
list_select_related = ('parent',)
view_on_site = False
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.19 on 2025-03-10 15:51

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('abuse', '0050_contentdecision_metadata'),
]

operations = [
migrations.AddField(
model_name='cinderpolicy',
name='enforcement_actions',
field=models.JSONField(default=list, null=True),
),
migrations.AlterField(
model_name='contentdecision',
name='action',
field=models.PositiveSmallIntegerField(choices=[(1, 'User ban'), (2, 'Add-on disable'), (3, 'Forward add-on to reviewers'), (5, 'Rating delete'), (6, 'Collection delete'), (7, 'Approved (no action)'), (8, 'Add-on version reject'), (9, 'Add-on version delayed reject warning'), (10, 'Approved (new version approval)'), (11, 'Invalid report, so ignored'), (12, 'Content already removed (no action)'), (13, 'Forward add-on to legal'), (14, 'Pending rejection date changed')]),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.19 on 2025-03-12 11:36

from django.db import migrations



def fill_enforcement_actions(apps, schema_editor):
ACTIONS = {7: 'amo-approve', 11: 'amo-ignore'}
CinderPolicy = apps.get_model('abuse', 'CinderPolicy')
for policy in CinderPolicy.objects.filter(default_cinder_action__isnull=False):
if api_value := ACTIONS.get(policy.default_cinder_action):
policy.update(enforcement_actions=[api_value])


class Migration(migrations.Migration):

dependencies = [
('abuse', '0051_add_cinderpolicy_enforcement_actions'),
]

operations = [
migrations.RunPython(fill_enforcement_actions, migrations.RunPython.noop)
]
60 changes: 20 additions & 40 deletions src/olympia/abuse/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,8 @@
from olympia.versions.models import Version, VersionReviewerFlags

from .actions import (
ContentActionAlreadyRemoved,
ContentActionApproveInitialDecision,
ContentActionApproveNoAction,
ContentActionBanUser,
ContentActionChangePendingRejectionDate,
ContentActionDeleteCollection,
ContentActionDeleteRating,
ContentActionDisableAddon,
ContentActionForwardToLegal,
ContentActionForwardToReviewers,
ContentActionIgnore,
ContentActionNotImplemented,
CONTENT_ACTION_FROM_DECISION_ACTION,
ContentActionOverrideApprove,
ContentActionRejectVersion,
ContentActionRejectVersionDelayed,
ContentActionTargetAppealApprove,
ContentActionTargetAppealRemovalAffirmation,
)
Expand Down Expand Up @@ -892,9 +879,7 @@ class CinderPolicy(ModelBase):
related_name='children',
)
expose_in_reviewer_tools = models.BooleanField(default=False)
default_cinder_action = models.PositiveSmallIntegerField(
choices=DECISION_ACTIONS.choices, null=True, blank=True
)
enforcement_actions = models.JSONField(default=list, null=True)
present_in_cinder = models.BooleanField(null=True)

objects = CinderPolicyQuerySet.as_manager()
Expand All @@ -916,6 +901,23 @@ def full_text(self, canned_response_text=None):
class Meta:
verbose_name_plural = 'Cinder Policies'

@classmethod
def get_decision_actions_from_policies(cls, policies, *, for_entity=None):
actions = {
action.value
for policy in policies
for api_value in policy.enforcement_actions
if policy.enforcement_actions
and DECISION_ACTIONS.has_api_value(api_value)
and (action := DECISION_ACTIONS.for_api_value(api_value))
and (
not for_entity
or for_entity
in CONTENT_ACTION_FROM_DECISION_ACTION[action.value].valid_targets
)
}
return list(actions)


class ContentDecisionManager(ManagerBase):
def awaiting_action(self):
Expand Down Expand Up @@ -1070,31 +1072,9 @@ def get_target_display(self):
def is_third_party_initiated(self):
return bool((job := self.originating_job) and job.all_abuse_reports)

@classmethod
def get_action_helper_class(cls, decision_action):
return {
DECISION_ACTIONS.AMO_BAN_USER: ContentActionBanUser,
DECISION_ACTIONS.AMO_DISABLE_ADDON: ContentActionDisableAddon,
DECISION_ACTIONS.AMO_REJECT_VERSION_ADDON: ContentActionRejectVersion,
DECISION_ACTIONS.AMO_REJECT_VERSION_WARNING_ADDON: (
ContentActionRejectVersionDelayed
),
DECISION_ACTIONS.AMO_ESCALATE_ADDON: ContentActionForwardToReviewers,
DECISION_ACTIONS.AMO_DELETE_COLLECTION: ContentActionDeleteCollection,
DECISION_ACTIONS.AMO_DELETE_RATING: ContentActionDeleteRating,
DECISION_ACTIONS.AMO_APPROVE: ContentActionApproveNoAction,
DECISION_ACTIONS.AMO_APPROVE_VERSION: ContentActionApproveInitialDecision,
DECISION_ACTIONS.AMO_IGNORE: ContentActionIgnore,
DECISION_ACTIONS.AMO_CLOSED_NO_ACTION: ContentActionAlreadyRemoved,
DECISION_ACTIONS.AMO_LEGAL_FORWARD: ContentActionForwardToLegal,
DECISION_ACTIONS.AMO_CHANGE_PENDING_REJECTION_DATE: (
ContentActionChangePendingRejectionDate
),
}.get(decision_action, ContentActionNotImplemented)

def get_action_helper(self):
# Base case when it's a new decision, that wasn't an appeal
ContentActionClass = self.get_action_helper_class(self.action)
ContentActionClass = CONTENT_ACTION_FROM_DECISION_ACTION[self.action]
skip_reporter_notify = False
any_cinder_job = self.originating_job

Expand Down
7 changes: 7 additions & 0 deletions src/olympia/abuse/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from olympia.amo.celery import task
from olympia.amo.decorators import use_primary_db
from olympia.amo.utils import to_language
from olympia.constants.abuse import DECISION_ACTIONS
from olympia.reviewers.models import NeedsHumanReview, UsageTier
from olympia.users.models import UserProfile

Expand Down Expand Up @@ -171,6 +172,11 @@ def sync_policies(data, parent_id=None):
# If the policy is labelled, but not for AMO, skip it
continue
policies_in_cinder.add(policy['uuid'])
actions = [
action['slug']
for action in policy.get('enforcement_actions', [])
if DECISION_ACTIONS.has_api_value(action['slug'])
]
cinder_policy, _ = CinderPolicy.objects.update_or_create(
uuid=policy['uuid'],
defaults={
Expand All @@ -179,6 +185,7 @@ def sync_policies(data, parent_id=None):
'parent_id': parent_id,
'modified': datetime.now(),
'present_in_cinder': True,
'enforcement_actions': actions,
},
)

Expand Down
48 changes: 45 additions & 3 deletions src/olympia/abuse/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
from olympia.core import set_user
from olympia.ratings.models import Rating
from olympia.reviewers.models import NeedsHumanReview
from olympia.users.models import UserProfile
from olympia.versions.models import Version, VersionReviewerFlags

from ..actions import (
CONTENT_ACTION_FROM_DECISION_ACTION,
ContentActionBanUser,
ContentActionDeleteCollection,
ContentActionDeleteRating,
Expand Down Expand Up @@ -1953,6 +1955,46 @@ def test_without_parents_if_their_children_are_present(self):
lone_policy,
}

def test_get_decision_actions_from_policies(self):
policies = (
# no actions, ignored
CinderPolicy.objects.create(uuid='1', enforcement_actions=[]),
# multiple actions
CinderPolicy.objects.create(
uuid='2',
enforcement_actions=[
'amo-disable-addon',
'amo-approve',
'amo-ban-user',
],
),
# some duplicates, and unsupported actions
CinderPolicy.objects.create(
uuid='3', enforcement_actions=['amo-disable-addon', 'not-amo-action']
),
)
assert sorted(CinderPolicy.get_decision_actions_from_policies(policies)) == [
DECISION_ACTIONS.AMO_BAN_USER,
DECISION_ACTIONS.AMO_DISABLE_ADDON,
DECISION_ACTIONS.AMO_APPROVE,
]

assert sorted(
CinderPolicy.get_decision_actions_from_policies(policies, for_entity=Addon)
) == [
DECISION_ACTIONS.AMO_DISABLE_ADDON,
DECISION_ACTIONS.AMO_APPROVE,
]

assert sorted(
CinderPolicy.get_decision_actions_from_policies(
policies, for_entity=UserProfile
)
) == [
DECISION_ACTIONS.AMO_BAN_USER,
DECISION_ACTIONS.AMO_APPROVE,
]


class TestContentDecisionManager(TestCase):
def test_held_for_2nd_level_approval(self):
Expand Down Expand Up @@ -2121,7 +2163,7 @@ def test_get_action_helper(self):
},
}
action_to_class = [
(decision_action, ContentDecision.get_action_helper_class(decision_action))
(decision_action, CONTENT_ACTION_FROM_DECISION_ACTION[decision_action])
for decision_action in DECISION_ACTIONS.values
]
# base cases, where it's a decision without an override or appeal involved
Expand Down Expand Up @@ -2188,7 +2230,7 @@ def test_get_action_helper(self):
)

action_existing_to_class_no_reporter_emails = {
(action, action): ContentDecision.get_action_helper_class(action)
(action, action): CONTENT_ACTION_FROM_DECISION_ACTION[action]
for action in DECISION_ACTIONS.REMOVING.values
}
for (
Expand Down Expand Up @@ -2245,7 +2287,7 @@ def test_get_action_helper_override(self):

# But if there is no action_date the override is ignored
action_existing_to_class[(approve_action, action, None, None)] = (
ContentDecision.get_action_helper_class(approve_action)
CONTENT_ACTION_FROM_DECISION_ACTION[approve_action]
)

# Previous decisions are also considered though
Expand Down
33 changes: 33 additions & 0 deletions src/olympia/abuse/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,10 @@ def setUp(self):
'name': 'test-name',
'description': 'test-description',
'nested_policies': [],
'enforcement_actions': [
{'slug': 'amo-disable-addon'},
{'slug': 'amo-ban-user'},
],
}

def test_sync_cinder_policies_headers(self):
Expand Down Expand Up @@ -1103,6 +1107,35 @@ def test_only_amo_labelled_policies_added(self):
assert CinderPolicy.objects.count() == 6
assert CinderPolicy.objects.filter(text='ADDED').count() == 6

def test_enforcement_actions_synced(self):
data = [
{
'uuid': 'no-actions',
'name': 'no actions',
'description': '',
'enforcement_actions': [],
},
{
'uuid': 'multiple',
'name': 'multiple',
'description': '',
'enforcement_actions': [
{'slug': 'amo-disable-addon'},
{'slug': 'amo-approve'},
{'slug': 'amo-ban-user'},
{'slug': 'some-unsupported-action'},
],
},
]

responses.add(responses.GET, self.url, json=data, status=200)
sync_cinder_policies.delay()
assert CinderPolicy.objects.get(uuid='multiple').enforcement_actions == [
'amo-disable-addon',
'amo-approve',
'amo-ban-user',
]


def do_handle_escalate_action(*, from_2nd_level, expected_nhr_reason):
user_factory(id=settings.TASK_USER_ID)
Expand Down
3 changes: 2 additions & 1 deletion src/olympia/abuse/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from olympia.ratings.views import RatingViewSet
from olympia.users.models import UserProfile

from .actions import CONTENT_ACTION_FROM_DECISION_ACTION
from .cinder import CinderAddon
from .forms import AbuseAppealEmailForm, AbuseAppealForm
from .models import AbuseReport, CinderJob, ContentDecision
Expand Down Expand Up @@ -178,7 +179,7 @@ def filter_enforcement_actions(enforcement_actions, cinder_job):
if DECISION_ACTIONS.has_api_value(action_slug)
and (action := DECISION_ACTIONS.for_api_value(action_slug))
and target.__class__
in ContentDecision.get_action_helper_class(action.value).valid_targets
in CONTENT_ACTION_FROM_DECISION_ACTION[action.value].valid_targets
]


Expand Down
2 changes: 1 addition & 1 deletion src/olympia/reviewers/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ def clean(self):
if self.cleaned_data.get('cinder_jobs_to_resolve') and self.cleaned_data.get(
'cinder_policies'
):
actions = self.helper.handler.get_cinder_actions_from_policies(
actions = self.helper.handler.get_decision_actions_from_policies(
self.cleaned_data.get('cinder_policies')
)
if len(actions) == 0:
Expand Down
Loading
Loading