Rafał Pitoń 11 лет назад
Родитель
Сommit
9ef857f988

+ 1 - 1
misago/conf/defaults.py

@@ -116,10 +116,10 @@ MIDDLEWARE_CLASSES = (
     'misago.users.middleware.UserMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'misago.core.middleware.threadstore.ThreadStoreMiddleware',
     'misago.core.middleware.exceptionhandler.ExceptionHandlerMiddleware',
     'misago.users.middleware.OnlineTrackerMiddleware',
     'misago.admin.middleware.AdminAuthMiddleware',
+    'misago.core.middleware.threadstore.ThreadStoreMiddleware',
 )
 
 TEMPLATE_CONTEXT_PROCESSORS = (

+ 2 - 1
misago/users/authbackends.py

@@ -22,6 +22,7 @@ class MisagoBackend(ModelBackend):
         UserModel = get_user_model()
         try:
             manager = UserModel._default_manager
-            return manager.select_related('online_tracker').get(pk=user_id)
+            relations = ('online_tracker', 'ban_cache')
+            return manager.select_related(**relations).get(pk=user_id)
         except UserModel.DoesNotExist:
             return None

+ 79 - 9
misago/users/bans.py

@@ -1,21 +1,91 @@
+
+"""
+API for testing values for bans
+
+Calling this instead of Ban.objects.find_ban is preffered, if you don't want
+to use validate_X_banned validators
+"""
 from datetime import date, datetime
 
 from misago.core import cachebuster
-from misago.users.models import Ban
+from misago.users.models import Ban, BanCache
 
 
-"""
-Utils for checking bans
-"""
 BAN_CACHE_SESSION_KEY = 'misago_ip_check'
 BAN_VERSION_KEY = 'misago_bans'
 
 
-def is_user_banned(user):
-    pass
+def get_username_ban(username):
+    try:
+        return Ban.objects.find_ban(username=username)
+    except Ban.DoesNotExist:
+        return None
+
+
+def get_email_ban(email):
+    try:
+        return Ban.objects.find_ban(email=email)
+    except Ban.DoesNotExist:
+        return None
+
+
+def get_ip_ban(ip):
+    try:
+        return Ban.objects.find_ban(ip=ip)
+    except Ban.DoesNotExist:
+        return None
 
 
-def is_ip_banned(request):
+def get_user_ban(user):
+    """
+    This function checks if user is banned
+
+    When user model is available, this is preffered to calling
+    get_email_ban(user.email) and get_username_ban(user.username)
+    because it sets ban cache on user model
+    """
+    try:
+        ban_cache = user.ban_cache
+        if not ban_cache.is_valid:
+            _set_user_ban_cache(user)
+    except BanCache.DoesNotExist:
+        user.ban_cache = BanCache(user=user)
+        ban_cache = _set_user_ban_cache(user)
+
+    if ban_cache.is_banned:
+        return ban_cache
+    else:
+        return None
+
+
+def _set_user_ban_cache(user):
+    ban_cache = user.ban_cache
+    ban_cache.bans_version = cachebuster.get_version(BAN_VERSION_KEY)
+
+    try:
+        user_ban = Ban.objects.find_ban(username=user.username,
+                                        email=user.email)
+        ban_cache.is_banned = True
+        ban_cache.valid_until = user_ban.valid_until
+        ban_cache.user_message = user_ban.user_message
+        ban_cache.staff_message = user_ban.staff_message
+    except Ban.DoesNotExist:
+        ban_cache.is_banned = False
+        ban_cache.valid_until = None
+        ban_cache.user_message = None
+        ban_cache.staff_message = None
+
+    ban_cache.save()
+    return ban_cache
+
+
+"""
+Utility for checking if request came from banned IP
+
+This check may be performed frequently, which is why there is extra
+boilerplate that caches ban check result in session
+"""
+def get_request_ip_ban(request):
     session_ban_cache = _get_session_bancache(request)
     if session_ban_cache:
         if session_ban_cache['is_banned']:
@@ -23,7 +93,7 @@ def is_ip_banned(request):
         else:
             return False
 
-    found_ban = Ban.objects.find_ban(ip=request._misago_real_ip)
+    found_ban = get_ip_ban(request._misago_real_ip)
 
     ban_cache = request.session[BAN_CACHE_SESSION_KEY] = {
         'version': cachebuster.get_version(BAN_VERSION_KEY),
@@ -46,7 +116,7 @@ def is_ip_banned(request):
     else:
         ban_cache['is_banned'] = False
         request.session[BAN_CACHE_SESSION_KEY] = ban_cache
-        return False
+        return None
 
 
 def _get_session_bancache(request):

+ 2 - 2
misago/users/decorators.py

@@ -2,7 +2,7 @@ from django.core.exceptions import PermissionDenied
 from django.template.defaultfilters import date as format_date
 from django.utils.translation import gettext_lazy as _
 
-from misago.users.bans import is_ip_banned
+from misago.users.bans import get_request_ip_ban
 
 
 def deny_authenticated(f):
@@ -27,7 +27,7 @@ def deny_guests(f):
 
 def deny_banned_ips(f):
     def decorator(request, *args, **kwargs):
-        ban = is_ip_banned(request)
+        ban = get_request_ip_ban(request)
         if ban:
             default_message = _("Your IP address has been banned.")
             ban_message = ban.get('message') or default_message

+ 6 - 0
misago/users/middleware.py

@@ -1,4 +1,7 @@
+from django.contrib.auth import logout
 from django.utils import timezone
+
+from misago.users.bans import get_user_ban
 from misago.users.models import AnonymousUser, Online
 
 
@@ -15,6 +18,9 @@ class UserMiddleware(object):
     def process_request(self, request):
         if request.user.is_anonymous():
             request.user = AnonymousUser()
+        else:
+            if get_request_ip_ban(request) or get_user_ban(request.user):
+                logout(request)
 
 
 class OnlineTrackerMiddleware(object):

+ 2 - 0
misago/users/migrations/0001_initial.py

@@ -136,6 +136,8 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name='BanCache',
             fields=[
+                ('user_message', models.TextField(null=True, blank=True)),
+                ('staff_message', models.TextField(null=True, blank=True)),
                 ('bans_version', models.PositiveIntegerField(default=0)),
                 ('valid_until', models.DateField(null=True, blank=True)),
                 ('is_banned', models.BooleanField(default=False)),

+ 17 - 2
misago/users/models/bans.py

@@ -1,3 +1,4 @@
+from datetime import date
 import re
 
 from django.conf import settings
@@ -5,6 +6,7 @@ from django.db import models
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 
+from misago.core import cachebuster
 from misago.core.utils import time_amount
 
 
@@ -61,7 +63,8 @@ class BansManager(models.Manager):
                 return ban
             elif ban.test == BAN_IP and ip and ban.test_value(ip):
                 return ban
-        return None
+        else:
+            raise Ban.DoesNotExist('no valid ban for values has been found')
 
 
 class Ban(models.Model):
@@ -104,7 +107,19 @@ class Ban(models.Model):
 
 
 class BanCache(models.Model):
-    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True)
+    user = models.OneToOneField(
+        settings.AUTH_USER_MODEL, primary_key=True, related_name='ban_cache')
     is_banned = models.BooleanField(default=False)
     bans_version = models.PositiveIntegerField(default=0)
+    user_message = models.TextField(null=True, blank=True)
+    staff_message = models.TextField(null=True, blank=True)
     valid_until = models.DateField(null=True, blank=True)
+
+    @property
+    def is_valid(self):
+        version_is_valid = cachebuster.is_valid('misago_bans',
+                                                self.bans_version)
+        not_expired = not self.valid_until or self.valid_until < date.today()
+
+        return version_is_valid and not_expired
+

+ 3 - 2
misago/users/models/user.py

@@ -14,8 +14,6 @@ from misago.core.utils import slugify
 
 from misago.users.models.rank import Rank
 from misago.users.utils import hash_email
-from misago.users.validators import (validate_email, validate_password,
-                                     validate_username)
 
 
 __all__ = [
@@ -53,6 +51,9 @@ AUTO_SUBSCRIBE_CHOICES = (
 
 class UserManager(BaseUserManager):
     def create_user(self, username, email, password=None, **extra_fields):
+        from misago.users.validators import (validate_email, validate_password,
+                                             validate_username)
+
         with transaction.atomic():
             if not email:
                 raise ValueError(_("User must have an email address."))

+ 8 - 4
misago/users/tests/test_ban_model.py

@@ -14,17 +14,20 @@ class BansManagerTests(TestCase):
     def test_find_ban_for_banned_name(self):
         """find_ban finds ban for given username"""
         self.assertTrue(Ban.objects.find_ban(username='Bob') != None)
-        self.assertTrue(Ban.objects.find_ban(username='Jeb') == None)
+        with self.assertRaises(Ban.DoesNotExist):
+            Ban.objects.find_ban(username='Jeb')
 
     def test_find_ban_for_banned_email(self):
         """find_ban finds ban for given email"""
         self.assertTrue(Ban.objects.find_ban(email='bob@test.com') != None)
-        self.assertTrue(Ban.objects.find_ban(email='jeb@test.com') == None)
+        with self.assertRaises(Ban.DoesNotExist):
+            Ban.objects.find_ban(email='jeb@test.com')
 
     def test_find_ban_for_banned_ip(self):
         """find_ban finds ban for given ip"""
         self.assertTrue(Ban.objects.find_ban(ip='127.0.0.1') != None)
-        self.assertTrue(Ban.objects.find_ban(ip='42.0.0.1') == None)
+        with self.assertRaises(Ban.DoesNotExist):
+            Ban.objects.find_ban(ip='42.0.0.1')
 
     def test_find_ban_for_all_bans(self):
         """find_ban finds ban for given values"""
@@ -32,7 +35,8 @@ class BansManagerTests(TestCase):
         self.assertTrue(Ban.objects.find_ban(**valid_kwargs) != None)
 
         invalid_kwargs = {'username': 'bsob', 'ip': '42.51.52.51'}
-        self.assertTrue(Ban.objects.find_ban(**invalid_kwargs) == None)
+        with self.assertRaises(Ban.DoesNotExist):
+            Ban.objects.find_ban(**invalid_kwargs)
 
 
 class BanTests(TestCase):

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

@@ -0,0 +1,99 @@
+from datetime import date, timedelta
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+
+from misago.users.bans import get_user_ban, get_request_ip_ban
+from misago.users.models import Ban, BAN_IP
+
+
+class UserBansTests(TestCase):
+    def setUp(self):
+        User = get_user_model()
+        self.user = User.objects.create_user('Bob',
+                                             'bob@boberson.com',
+                                             'pass123')
+
+    def test_no_ban(self):
+        """user is not caught by ban"""
+        self.assertIsNone(get_user_ban(self.user))
+        self.assertFalse(self.user.ban_cache.is_banned)
+
+    def test_permanent_ban(self):
+        """user is caught by permanent ban"""
+        Ban.objects.create(banned_value='bob',
+                           user_message='User reason',
+                           staff_message='Staff reason')
+
+        user_ban = get_user_ban(self.user)
+        self.assertIsNotNone(user_ban)
+        self.assertEqual(user_ban.user_message, 'User reason')
+        self.assertEqual(user_ban.staff_message, 'Staff reason')
+        self.assertTrue(self.user.ban_cache.is_banned)
+
+    def test_temporary_ban(self):
+        """user is caught by temporary ban"""
+        Ban.objects.create(banned_value='bo*',
+                           user_message='User reason',
+                           staff_message='Staff reason',
+                           valid_until=date.today() + timedelta(days=7))
+
+        user_ban = get_user_ban(self.user)
+        self.assertIsNotNone(user_ban)
+        self.assertEqual(user_ban.user_message, 'User reason')
+        self.assertEqual(user_ban.staff_message, 'Staff reason')
+        self.assertTrue(self.user.ban_cache.is_banned)
+
+    def test_expired_ban(self):
+        """user is not caught by expired ban"""
+        Ban.objects.create(banned_value='bo*',
+                           valid_until=date.today() - timedelta(days=7))
+
+        self.assertIsNone(get_user_ban(self.user))
+        self.assertFalse(self.user.ban_cache.is_banned)
+
+
+class FakeRequest(object):
+    def __init__(self):
+        self._misago_real_ip = '127.0.0.1'
+        self.session = {}
+
+
+class RequestIPBansTests(TestCase):
+    def test_no_ban(self):
+        """no ban found"""
+        ip_ban = get_request_ip_ban(FakeRequest())
+        self.assertIsNone(ip_ban)
+
+    def test_permanent_ban(self):
+        """ip is caught by permanent ban"""
+        Ban.objects.create(test=BAN_IP,
+                           banned_value='127.0.0.1',
+                           user_message='User reason')
+
+        ip_ban = get_request_ip_ban(FakeRequest())
+        self.assertTrue(ip_ban['is_banned'])
+        self.assertEqual(ip_ban['ip'], '127.0.0.1')
+        self.assertEqual(ip_ban['message'], 'User reason')
+
+    def test_temporary_ban(self):
+        """ip is caught by temporary ban"""
+        Ban.objects.create(test=BAN_IP,
+                           banned_value='127.0.0.1',
+                           user_message='User reason',
+                           valid_until=date.today() + timedelta(days=7))
+
+        ip_ban = get_request_ip_ban(FakeRequest())
+        self.assertTrue(ip_ban['is_banned'])
+        self.assertEqual(ip_ban['ip'], '127.0.0.1')
+        self.assertEqual(ip_ban['message'], 'User reason')
+
+    def test_expired_ban(self):
+        """ip is not caught by expired ban"""
+        Ban.objects.create(test=BAN_IP,
+                           banned_value='127.0.0.1',
+                           user_message='User reason',
+                           valid_until=date.today() - timedelta(days=7))
+
+        ip_ban = get_request_ip_ban(FakeRequest())
+        self.assertIsNone(ip_ban)

+ 9 - 10
misago/users/validators.py

@@ -6,6 +6,7 @@ from django.utils.translation import ungettext, ugettext_lazy as _
 from django.contrib.auth import get_user_model
 
 from misago.conf import settings
+from misago.users.bans import get_email_ban, get_username_ban
 
 
 USERNAME_RE = re.compile(r'^[0-9a-z]+$', re.IGNORECASE)
@@ -25,12 +26,11 @@ def validate_email_available(value, exclude=None):
 
 
 def validate_email_banned(value):
-    from misago.users.models import Ban
-    email_ban = Ban.objects.find_ban(email=value)
+    ban = get_email_ban(value)
 
-    if email_ban:
-        if email_ban.user_message:
-            raise ValidationError(email_ban.user_message)
+    if ban:
+        if ban.user_message:
+            raise ValidationError(ban.user_message)
         else:
             raise ValidationError(_("This e-mail address is not allowed."))
 
@@ -69,12 +69,11 @@ def validate_username_available(value, exclude=None):
 
 
 def validate_username_banned(value):
-    from misago.users.models import Ban
-    username_ban = Ban.objects.find_ban(username=value)
+    ban = get_username_ban(value)
 
-    if username_ban:
-        if username_ban.user_message:
-            raise ValidationError(username_ban.user_message)
+    if ban:
+        if ban.user_message:
+            raise ValidationError(ban.user_message)
         else:
             raise ValidationError(_("This username is not allowed."))