Browse Source

#555: implementation, admin, api tests

Rafał Pitoń 8 years ago
parent
commit
3beac665fb

+ 1 - 0
misago/templates/misago/admin/bans/form.html

@@ -44,6 +44,7 @@ class="form-horizontal"
 
     {% form_row form.check_type label_class field_class %}
     {% form_row form.banned_value label_class field_class %}
+    {% form_row form.registration_only label_class field_class %}
     {% form_row form.expires_on label_class field_class %}
 
   </fieldset>

+ 13 - 4
misago/templates/misago/admin/bans/list.html

@@ -13,8 +13,8 @@
 
 
 {% block table-header %}
-<th style="width: 25%;">{% trans "Ban" %}</th>
-<th style="width: 160px;">{% trans "Type" %}</th>
+<th style="width: 30%;">{% trans "Ban" %}</th>
+<th style="width: 30%;">{% trans "Type" %}</th>
 <th>{% trans "Expires on" %}</th>
 {% for action in extra_actions %}
 <th style="width: 1%;">&nbsp;</th>
@@ -29,7 +29,13 @@
   {{ item.banned_value }}
 </td>
 <td>
-  {{ item.test_name }}
+  {% if item.registration_only %}
+    {% blocktrans trimmed with check_type=item.get_check_type_display %}
+      {{ check_type }}, registration only
+    {% endblocktrans %}
+  {% else %}
+    {{ item.get_check_type_display }}
+  {% endif %}
 </td>
 <td{% if item.is_expired %} class="text-muted"{% endif %}>
   {% if item.expires_on %}
@@ -100,7 +106,10 @@
   </div>
 </div>
 <div class="row">
-  <div class="col-md-12">
+  <div class="col-md-6">
+    {% form_row search_form.registration_only %}
+  </div>
+  <div class="col-md-6">
     {% form_row search_form.state %}
   </div>
 </div>

+ 6 - 6
misago/users/bans.py

@@ -18,23 +18,23 @@ CACHE_SESSION_KEY = 'misago_ip_check'
 VERSION_KEY = 'misago_bans'
 
 
-def get_username_ban(username):
+def get_username_ban(username, registration_only=False):
     try:
-        return Ban.objects.get_username_ban(username)
+        return Ban.objects.get_username_ban(username, registration_only)
     except Ban.DoesNotExist:
         return None
 
 
-def get_email_ban(email):
+def get_email_ban(email, registration_only=False):
     try:
-        return Ban.objects.get_email_ban(email)
+        return Ban.objects.get_email_ban(email, registration_only)
     except Ban.DoesNotExist:
         return None
 
 
-def get_ip_ban(ip):
+def get_ip_ban(ip, registration_only=False):
     try:
-        return Ban.objects.get_ip_ban(ip)
+        return Ban.objects.get_ip_ban(ip, registration_only)
     except Ban.DoesNotExist:
         return None
 

+ 27 - 5
misago/users/forms/admin.py

@@ -457,7 +457,15 @@ class BanUsersForm(forms.Form):
 
 
 class BanForm(forms.ModelForm):
-    check_type = forms.TypedChoiceField(label=_("Check type"), coerce=int, choices=Ban.CHOICES)
+    check_type = forms.TypedChoiceField(label=_("Check type"), coerce=int, choices=Ban.CHOICES,)
+    registration_only = YesNoSwitch(
+        label=_("Restrict this ban to registrations"),
+        help_text=_(
+            "Changing this to yes will make this ban check be only performed on registration "
+            "step. This is good if you want to block certain registrations like ones from "
+            "recently comprimised e-mail providers, without harming existing users."
+        ),
+    )
     banned_value = forms.CharField(
         label=_("Banned value"),
         max_length=250,
@@ -500,6 +508,7 @@ class BanForm(forms.ModelForm):
         model = Ban
         fields = [
             'check_type',
+            'registration_only',
             'banned_value',
             'user_message',
             'staff_message',
@@ -518,15 +527,22 @@ class BanForm(forms.ModelForm):
 
 
 class SearchBansForm(forms.Form):
-    SARCH_CHOICES = [
+    check_type = forms.ChoiceField(label=_("Type"), required=False, choices=[
         ('', _('All bans')),
         ('names', _('Usernames')),
         ('emails', _('E-mails')),
         ('ips', _('IPs')),
-    ]
-
-    check_type = forms.ChoiceField(label=_("Type"), required=False, choices=SARCH_CHOICES)
+    ],)
     value = forms.CharField(label=_("Banned value begins with"), required=False)
+    registration_only = forms.ChoiceField(
+        label=_("Registration only"),
+        required=False,
+        choices=[
+            ('', _('Any')),
+            ('only', _('Yes')),
+            ('exclude', _('No')),
+        ]
+    )
     state = forms.ChoiceField(
         label=_("State"),
         required=False,
@@ -557,4 +573,10 @@ class SearchBansForm(forms.Form):
         if criteria.get('state') == 'unused':
             queryset = queryset.filter(is_checked=False)
 
+        if criteria.get('registration_only') == 'only':
+            queryset = queryset.filter(registration_only=True)
+
+        if criteria.get('registration_only') == 'exclude':
+            queryset = queryset.filter(registration_only=False)
+
         return queryset

+ 30 - 0
misago/users/forms/register.py

@@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.password_validation import validate_password
 
 from misago.users import validators
+from misago.users.bans import get_email_ban, get_username_ban
 
 
 UserModel = get_user_model()
@@ -20,6 +21,28 @@ class RegisterForm(forms.Form):
         self.request = kwargs.pop('request')
         super(RegisterForm, self).__init__(*args, **kwargs)
 
+    def clean_username(self):
+        data = self.cleaned_data['username']
+
+        ban = get_username_ban(data, registration_only=True)
+        if ban:
+            if ban.user_message:
+                raise ValidationError(ban.user_message)
+            else:
+                raise ValidationError(_("This usernane is not allowed."))
+        return data
+
+    def clean_email(self):
+        data = self.cleaned_data['email']
+
+        ban = get_email_ban(data, registration_only=True)
+        if ban:
+            if ban.user_message:
+                raise ValidationError(ban.user_message)
+            else:
+                raise ValidationError(_("This e-mail address is not allowed."))
+        return data
+
     def full_clean_password(self, cleaned_data):
         if cleaned_data.get('password'):
             validate_password(
@@ -33,6 +56,13 @@ class RegisterForm(forms.Form):
     def clean(self):
         cleaned_data = super(RegisterForm, self).clean()
 
+        ban = get_ip_ban(self.request.user_ip, registration_only=True)
+        if ban:
+            if ban.user_message:
+                raise ValidationError(ban.user_message)
+            else:
+                raise ValidationError(_("New registrations from this IP address are not allowed."))
+
         try:
             self.full_clean_password(cleaned_data)
         except forms.ValidationError as e:

+ 25 - 0
misago/users/migrations/0008_ban_registration_only.py

@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-03-04 22:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_users', '0007_auto_20170219_1639'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='ban',
+            name='registration_only',
+            field=models.BooleanField(db_index=True, default=False),
+        ),
+        migrations.AlterField(
+            model_name='ban',
+            name='check_type',
+            field=models.PositiveIntegerField(choices=[(0, 'Username'), (1, 'E-mail address'), (2, 'IP address')], db_index=True, default=0),
+        ),
+    ]

+ 23 - 13
misago/users/models/ban.py

@@ -10,19 +10,28 @@ from misago.users.constants import BANS_CACHEBUSTER
 
 
 class BansManager(models.Manager):
-    def get_ip_ban(self, ip):
-        return self.get_ban(ip=ip)
+    def get_ip_ban(self, ip, registration_only=False):
+        return self.get_ban(
+            ip=ip,
+            registration_only=registration_only,
+        )
 
-    def get_username_ban(self, username):
-        return self.get_ban(username=username)
+    def get_username_ban(self, username, registration_only=False):
+        return self.get_ban(
+            username=username,
+            registration_only=registration_only,
+        )
 
-    def get_email_ban(self, email):
-        return self.get_ban(email=email)
+    def get_email_ban(self, email, registration_only=False):
+        return self.get_ban(
+            email=email,
+            registration_only=registration_only,
+        )
 
     def invalidate_cache(self):
         cachebuster.invalidate(BANS_CACHEBUSTER)
 
-    def get_ban(self, username=None, email=None, ip=None):
+    def get_ban(self, username=None, email=None, ip=None, registration_only=False):
         checks = []
 
         if username:
@@ -34,7 +43,11 @@ class BansManager(models.Manager):
         if ip:
             checks.append(self.model.IP)
 
-        queryset = self.filter(is_checked=True)
+        queryset = self.filter(
+            is_checked=True,
+            registration_only=registration_only
+        )
+
         if len(checks) == 1:
             queryset = queryset.filter(check_type=checks[0])
         elif checks:
@@ -64,7 +77,8 @@ class Ban(models.Model):
         (IP, _('IP address')),
     ]
 
-    check_type = models.PositiveIntegerField(default=USERNAME, db_index=True)
+    check_type = models.PositiveIntegerField(default=USERNAME, choices=CHOICES, db_index=True)
+    registration_only = models.BooleanField(default=False, db_index=True)
     banned_value = models.CharField(max_length=255, db_index=True)
     user_message = models.TextField(null=True, blank=True)
     staff_message = models.TextField(null=True, blank=True)
@@ -84,10 +98,6 @@ class Ban(models.Model):
         return BanMessageSerializer(self).data
 
     @property
-    def check_name(self):
-        return self.CHOICES[self.check_type][1]
-
-    @property
     def name(self):
         return self.banned_value
 

+ 26 - 0
misago/users/tests/test_bans.py

@@ -40,6 +40,14 @@ class GetBanTests(TestCase):
         )
         self.assertEqual(get_username_ban('admiral').pk, valid_ban.pk)
 
+        regitration_ban = Ban.objects.create(
+            banned_value='bob*',
+            expires_on=timezone.now() + timedelta(days=7),
+            registration_only=True
+        )
+        self.assertIsNone(get_username_ban('boberson'))
+        self.assertEqual(get_username_ban('boberson', True).pk, regitration_ban.pk)
+
     def test_get_email_ban(self):
         """get_email_ban returns valid ban"""
         nonexistent_ban = get_email_ban('non@existent.com')
@@ -69,6 +77,15 @@ class GetBanTests(TestCase):
         )
         self.assertEqual(get_email_ban('banned@mail.ru').pk, valid_ban.pk)
 
+        regitration_ban = Ban.objects.create(
+            banned_value='*.ua',
+            check_type=Ban.EMAIL,
+            expires_on=timezone.now() + timedelta(days=7),
+            registration_only=True
+        )
+        self.assertIsNone(get_email_ban('banned@mail.ua'))
+        self.assertEqual(get_email_ban('banned@mail.ua', True).pk, regitration_ban.pk)
+
     def test_get_ip_ban(self):
         """get_ip_ban returns valid ban"""
         nonexistent_ban = get_ip_ban('123.0.0.1')
@@ -98,6 +115,15 @@ class GetBanTests(TestCase):
         )
         self.assertEqual(get_ip_ban('125.0.0.1').pk, valid_ban.pk)
 
+        regitration_ban = Ban.objects.create(
+            banned_value='188.*',
+            check_type=Ban.IP,
+            expires_on=timezone.now() + timedelta(days=7),
+            registration_only=True
+        )
+        self.assertIsNone(get_ip_ban('188.12.12.41'))
+        self.assertEqual(get_ip_ban('188.12.12.41', True).pk, regitration_ban.pk)
+
 
 class UserBansTests(TestCase):
     def setUp(self):