extensions-website/abuse/models.py
Anna Sirota 729ce453ac Fix deletion task, protect more account-linked data
Account deletion task had a bug in it: its was skipping all accounts
that were deactivated, but accounts are deactivated as soon as
date_deletion_requested is received via webhook, so no
deletion/anonymisation would have happened.

In addition to fixing that (now the task will exclude all account records
that look like they were already anonymized), this also changes several on-delete properties:

  * abuse reports about a deleted account are deleted along with it;
  * abuse reports made by a deleted account remain;
  * ratings made by a deleted account remain;
  * approval activity protects against deletion: account cannot be deleted if it authored any approval activity;

This change also makes sure that API tokens and OAuth info/tokens
are deleted when account deleted or anonymized.
2024-06-05 13:20:51 +02:00

112 lines
4.0 KiB
Python

from django import forms
from django.contrib.auth import get_user_model
from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
from django.core.validators import validate_ipv46_address
from django.db import models
from django.urls import reverse
from extended_choices import Choices
from geoip2.errors import GeoIP2Error
from constants.base import ABUSE_TYPE, ABUSE_TYPE_EXTENSION, ABUSE_TYPE_RATING
from common.model_mixins import CreatedModifiedMixin, TrackChangesMixin
import extensions.fields
User = get_user_model()
class AbuseReport(CreatedModifiedMixin, TrackChangesMixin, models.Model):
TYPE = ABUSE_TYPE
REASONS = Choices(
('OTHER', 127, 'Other'),
('DAMAGE', 1, 'Damages computer and/or data'),
('SPAM', 2, 'Creates spam or advertising'),
('BROKEN', 3, "Doesn't work, breaks Blender, or slows it down"),
('POLICY', 4, 'Hateful, violent, or illegal content'),
('DECEPTIVE', 5, "Pretends to be something it's not"),
)
STATUSES = Choices(
('UNTRIAGED', 1, 'Untriaged'),
('CONFIRMED', 2, 'Confirmed'),
('RESOLVED', 3, 'Resolved'),
)
# NULL if the reporter is anonymous.
# FIXME? make non-null
reporter = models.ForeignKey(
User,
null=True,
blank=True,
related_name='abuse_reported',
on_delete=models.SET_NULL,
)
# An abuse report can be for an extension or a user.
# If user is set then extension should be null.
# If extension is null then user should be set.
user = models.ForeignKey(
User, null=True, related_name='abuse_reports', on_delete=models.CASCADE
)
extension = models.ForeignKey(
'extensions.Extension', blank=True, null=True, on_delete=models.CASCADE
)
extension_version = extensions.fields.VersionStringField(max_length=64, null=True, blank=True)
rating = models.ForeignKey('ratings.Rating', blank=True, null=True, on_delete=models.CASCADE)
message = models.TextField(blank=True)
reason = models.PositiveSmallIntegerField(
default=REASONS.OTHER, choices=REASONS.choices, blank=False, null=False
)
version = extensions.fields.VersionStringField(
max_length=64,
null=True,
blank=True,
help_text=('Version of Blender affected by this report, if applicable.'),
)
type = models.PositiveSmallIntegerField(
default=ABUSE_TYPE_EXTENSION, choices=TYPE.choices, blank=False, null=False
)
status = models.PositiveSmallIntegerField(default=STATUSES.UNTRIAGED, choices=STATUSES.choices)
@classmethod
def lookup_country_code_from_ip(cls, ip):
try:
# Early check to avoid initializing GeoIP2 on invalid addresses
if not ip:
raise forms.ValidationError('No IP')
validate_ipv46_address(ip)
geoip = GeoIP2()
value = geoip.country_code(ip)
# Annoyingly, we have to catch both django's GeoIP2Exception (setup
# issue) and geoip2's GeoIP2Error (lookup issue)
except (forms.ValidationError, GeoIP2Exception, GeoIP2Error):
value = ''
return value
@property
def name(self) -> str:
return self.extension.name if self.extension else self.user
def __str__(self) -> str:
return f'Abuse Report for {self.type} {self.name}'
@classmethod
def exists(cls, user_id: int, extension_id: int, rating_id: int = None) -> bool:
if rating_id is None:
return cls.objects.filter(
reporter_id=user_id, extension_id=extension_id, type=ABUSE_TYPE_EXTENSION
).exists()
return cls.objects.filter(
reporter_id=user_id,
extension_id=extension_id,
rating_id=rating_id,
type=ABUSE_TYPE_RATING,
).exists()
def get_absolute_url(self):
return reverse('abuse:view-report', args=[self.pk])
def get_admin_url(self):
return reverse('admin:abuse_abusereport_change', args=[self.pk])