diff --git a/defender/admin.py b/defender/admin.py index 625c6be..8238f78 100644 --- a/defender/admin.py +++ b/defender/admin.py @@ -4,29 +4,26 @@ from .models import AccessAttempt class AccessAttemptAdmin(admin.ModelAdmin): """ Access attempt admin config """ + list_display = ( - 'attempt_time', - 'ip_address', - 'user_agent', - 'username', - 'path_info', - 'login_valid', + "attempt_time", + "ip_address", + "user_agent", + "username", + "path_info", + "login_valid", ) search_fields = [ - 'ip_address', - 'username', + "ip_address", + "username", ] - date_hierarchy = 'attempt_time' + date_hierarchy = "attempt_time" fieldsets = ( - (None, { - 'fields': ('path_info', 'login_valid') - }), - ('Meta Data', { - 'fields': ('user_agent', 'ip_address') - }) + (None, {"fields": ("path_info", "login_valid")}), + ("Meta Data", {"fields": ("user_agent", "ip_address")}), ) diff --git a/defender/config.py b/defender/config.py index a61a88a..18afde4 100644 --- a/defender/config.py +++ b/defender/config.py @@ -9,76 +9,78 @@ def get_setting(variable, default=None): # redis server host -DEFENDER_REDIS_URL = get_setting('DEFENDER_REDIS_URL') +DEFENDER_REDIS_URL = get_setting("DEFENDER_REDIS_URL") # reuse declared cache from django settings -DEFENDER_REDIS_NAME = get_setting('DEFENDER_REDIS_NAME') +DEFENDER_REDIS_NAME = get_setting("DEFENDER_REDIS_NAME") -MOCK_REDIS = get_setting('DEFENDER_MOCK_REDIS', False) +MOCK_REDIS = get_setting("DEFENDER_MOCK_REDIS", False) # see if the user has overridden the failure limit -FAILURE_LIMIT = get_setting('DEFENDER_LOGIN_FAILURE_LIMIT', 3) -USERNAME_FAILURE_LIMIT = get_setting('DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME', FAILURE_LIMIT) -IP_FAILURE_LIMIT = get_setting('DEFENDER_LOGIN_FAILURE_LIMIT_IP', FAILURE_LIMIT) +FAILURE_LIMIT = get_setting("DEFENDER_LOGIN_FAILURE_LIMIT", 3) +USERNAME_FAILURE_LIMIT = get_setting( + "DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME", FAILURE_LIMIT +) +IP_FAILURE_LIMIT = get_setting("DEFENDER_LOGIN_FAILURE_LIMIT_IP", FAILURE_LIMIT) # If this is True, the lockout checks to evaluate if the IP failure limit and # the username failure limit has been reached before issuing the lockout. -LOCKOUT_BY_IP_USERNAME = get_setting( - 'DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME', False) +LOCKOUT_BY_IP_USERNAME = get_setting("DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME", False) # if this is True, The users IP address will not get locked when # there are too many login attempts. -DISABLE_IP_LOCKOUT = get_setting('DEFENDER_DISABLE_IP_LOCKOUT', False) +DISABLE_IP_LOCKOUT = get_setting("DEFENDER_DISABLE_IP_LOCKOUT", False) # If this is True, usernames will not get locked when # there are too many login attempts. -DISABLE_USERNAME_LOCKOUT = get_setting( - 'DEFENDER_DISABLE_USERNAME_LOCKOUT', False) +DISABLE_USERNAME_LOCKOUT = get_setting("DEFENDER_DISABLE_USERNAME_LOCKOUT", False) # use a specific username field to retrieve from login POST data -USERNAME_FORM_FIELD = get_setting('DEFENDER_USERNAME_FORM_FIELD', 'username') +USERNAME_FORM_FIELD = get_setting("DEFENDER_USERNAME_FORM_FIELD", "username") # see if the django app is sitting behind a reverse proxy -BEHIND_REVERSE_PROXY = get_setting('DEFENDER_BEHIND_REVERSE_PROXY', False) +BEHIND_REVERSE_PROXY = get_setting("DEFENDER_BEHIND_REVERSE_PROXY", False) # the prefix for these keys in your cache. -CACHE_PREFIX = get_setting('DEFENDER_CACHE_PREFIX', 'defender') +CACHE_PREFIX = get_setting("DEFENDER_CACHE_PREFIX", "defender") # if the django app is behind a reverse proxy, look for the # ip address using this HTTP header value -REVERSE_PROXY_HEADER = get_setting('DEFENDER_REVERSE_PROXY_HEADER', - 'HTTP_X_FORWARDED_FOR') +REVERSE_PROXY_HEADER = get_setting( + "DEFENDER_REVERSE_PROXY_HEADER", "HTTP_X_FORWARDED_FOR" +) try: # how long to wait before the bad login attempt gets forgotten. in seconds. - COOLOFF_TIME = int(get_setting('DEFENDER_COOLOFF_TIME', 300)) # seconds + COOLOFF_TIME = int(get_setting("DEFENDER_COOLOFF_TIME", 300)) # seconds except ValueError: # pragma: no cover - raise Exception( - 'DEFENDER_COOLOFF_TIME needs to be an integer') # pragma: no cover + raise Exception("DEFENDER_COOLOFF_TIME needs to be an integer") # pragma: no cover -LOCKOUT_TEMPLATE = get_setting('DEFENDER_LOCKOUT_TEMPLATE') +LOCKOUT_TEMPLATE = get_setting("DEFENDER_LOCKOUT_TEMPLATE") -ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. " - "Note that both fields are case-sensitive.") +ERROR_MESSAGE = ugettext_lazy( + "Please enter a correct username and password. " + "Note that both fields are case-sensitive." +) -LOCKOUT_URL = get_setting('DEFENDER_LOCKOUT_URL') +LOCKOUT_URL = get_setting("DEFENDER_LOCKOUT_URL") -USE_CELERY = get_setting('DEFENDER_USE_CELERY', False) +USE_CELERY = get_setting("DEFENDER_USE_CELERY", False) -STORE_ACCESS_ATTEMPTS = get_setting('DEFENDER_STORE_ACCESS_ATTEMPTS', True) +STORE_ACCESS_ATTEMPTS = get_setting("DEFENDER_STORE_ACCESS_ATTEMPTS", True) # Used by the management command to decide how long to keep access attempt # recods. Number is # of hours. try: - ACCESS_ATTEMPT_EXPIRATION = int(get_setting( - 'DEFENDER_ACCESS_ATTEMPT_EXPIRATION', 24)) + ACCESS_ATTEMPT_EXPIRATION = int( + get_setting("DEFENDER_ACCESS_ATTEMPT_EXPIRATION", 24) + ) except ValueError: # pragma: no cover raise Exception( - 'DEFENDER_ACCESS_ATTEMPT_EXPIRATION' - ' needs to be an integer') # pragma: no cover + "DEFENDER_ACCESS_ATTEMPT_EXPIRATION" " needs to be an integer" + ) # pragma: no cover GET_USERNAME_FROM_REQUEST_PATH = get_setting( - 'DEFENDER_GET_USERNAME_FROM_REQUEST_PATH', - 'defender.utils.username_from_request' + "DEFENDER_GET_USERNAME_FROM_REQUEST_PATH", "defender.utils.username_from_request" ) diff --git a/defender/connection.py b/defender/connection.py index 23e4a7e..172c6fe 100644 --- a/defender/connection.py +++ b/defender/connection.py @@ -2,6 +2,7 @@ from django.core.cache import caches from django.core.cache.backends.base import InvalidCacheBackendError import redis + try: import urlparse except ImportError: # pragma: no cover @@ -12,21 +13,20 @@ from . import config # Register database schemes in URLs. urlparse.uses_netloc.append("redis") -INVALID_CACHE_ERROR_MSG = 'The cache {} was not found on the django cache' \ - ' settings.' +INVALID_CACHE_ERROR_MSG = "The cache {} was not found on the django cache" " settings." def get_redis_connection(): """ Get the redis connection if not using mock """ if config.MOCK_REDIS: # pragma: no cover import mockredis + return mockredis.mock_strict_redis_client() # pragma: no cover elif config.DEFENDER_REDIS_NAME: # pragma: no cover try: cache = caches[config.DEFENDER_REDIS_NAME] except InvalidCacheBackendError: - raise KeyError(INVALID_CACHE_ERROR_MSG.format( - config.DEFENDER_REDIS_NAME)) + raise KeyError(INVALID_CACHE_ERROR_MSG.format(config.DEFENDER_REDIS_NAME)) # every redis backend implement it own way to get the low level client try: # redis_cache.RedisCache case (django-redis-cache package) @@ -37,12 +37,12 @@ def get_redis_connection(): else: # pragma: no cover redis_config = parse_redis_url(config.DEFENDER_REDIS_URL) return redis.StrictRedis( - host=redis_config.get('HOST'), - port=redis_config.get('PORT'), - db=redis_config.get('DB'), - password=redis_config.get('PASSWORD'), - ssl=redis_config.get('SSL')) - + host=redis_config.get("HOST"), + port=redis_config.get("PORT"), + db=redis_config.get("DB"), + password=redis_config.get("PASSWORD"), + ssl=redis_config.get("SSL"), + ) def parse_redis_url(url): @@ -54,7 +54,7 @@ def parse_redis_url(url): "PASSWORD": None, "HOST": "localhost", "PORT": 6379, - "SSL": False + "SSL": False, } if not url: @@ -63,7 +63,7 @@ def parse_redis_url(url): url = urlparse.urlparse(url) # Remove query strings. path = url.path[1:] - path = path.split('?', 2)[0] + path = path.split("?", 2)[0] if path: redis_config.update({"DB": int(path)}) @@ -73,7 +73,7 @@ def parse_redis_url(url): redis_config.update({"HOST": url.hostname}) if url.port: redis_config.update({"PORT": int(url.port)}) - if url.scheme in ['https', 'rediss']: + if url.scheme in ["https", "rediss"]: redis_config.update({"SSL": True}) return redis_config diff --git a/defender/data.py b/defender/data.py index 8febfb9..31e64db 100644 --- a/defender/data.py +++ b/defender/data.py @@ -1,8 +1,9 @@ from .models import AccessAttempt -def store_login_attempt(user_agent, ip_address, username, - http_accept, path_info, login_valid): +def store_login_attempt( + user_agent, ip_address, username, http_accept, path_info, login_valid +): """ Store the login attempt to the db. """ AccessAttempt.objects.create( user_agent=user_agent, diff --git a/defender/decorators.py b/defender/decorators.py index bb4b004..3612f2a 100644 --- a/defender/decorators.py +++ b/defender/decorators.py @@ -3,8 +3,7 @@ from . import utils import functools -def watch_login(status_code=302, msg='', - get_username=utils.get_username_from_request): +def watch_login(status_code=302, msg="", get_username=utils.get_username_from_request): """ Used to decorate the django.contrib.admin.site.login method or any other function you want to protect by brute forcing. @@ -12,6 +11,7 @@ def watch_login(status_code=302, msg='', indicate a failure and/or a string that will be checked within the response body. """ + def decorated_login(func): @functools.wraps(func) def wrapper(request, *args, **kwargs): @@ -24,29 +24,30 @@ def watch_login(status_code=302, msg='', # call the login function response = func(request, *args, **kwargs) - if request.method == 'POST': + if request.method == "POST": # see if the login was successful if status_code == 302: # standard Django login view login_unsuccessful = ( - response and - not response.has_header('location') and - response.status_code != status_code + response + and not response.has_header("location") + and response.status_code != status_code ) else: # If msg is not passed the last condition will be evaluated # always to True so the first 2 will decide the result. login_unsuccessful = ( - response and response.status_code == status_code - and msg in response.content.decode('utf-8') + response + and response.status_code == status_code + and msg in response.content.decode("utf-8") ) # ideally make this background task, but to keep simple, # keeping it inline for now. - utils.add_login_attempt_to_db(request, not login_unsuccessful, - get_username) + utils.add_login_attempt_to_db( + request, not login_unsuccessful, get_username + ) - if utils.check_request(request, login_unsuccessful, - get_username): + if utils.check_request(request, login_unsuccessful, get_username): return response return utils.lockout_response(request) @@ -54,4 +55,5 @@ def watch_login(status_code=302, msg='', return response return wrapper + return decorated_login diff --git a/defender/exampleapp/settings.py b/defender/exampleapp/settings.py index 4889801..9e97954 100644 --- a/defender/exampleapp/settings.py +++ b/defender/exampleapp/settings.py @@ -2,22 +2,21 @@ import os from celery import Celery PROJECT_DIR = lambda base: os.path.abspath( - os.path.join(os.path.dirname(__file__), base).replace('\\', '/')) - - -MEDIA_ROOT = PROJECT_DIR(os.path.join('media')) -MEDIA_URL = '/media/' -STATIC_ROOT = PROJECT_DIR(os.path.join('static')) -STATIC_URL = '/static/' - -STATICFILES_DIRS = ( - PROJECT_DIR(os.path.join('media', 'static')), + os.path.join(os.path.dirname(__file__), base).replace("\\", "/") ) + +MEDIA_ROOT = PROJECT_DIR(os.path.join("media")) +MEDIA_URL = "/media/" +STATIC_ROOT = PROJECT_DIR(os.path.join("static")) +STATIC_URL = "/static/" + +STATICFILES_DIRS = (PROJECT_DIR(os.path.join("media", "static")),) + DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': PROJECT_DIR('defender.sb'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": PROJECT_DIR("defender.sb"), } } @@ -25,35 +24,35 @@ DATABASES = { SITE_ID = 1 MIDDLEWARE = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'defender.middleware.FailedLoginMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "defender.middleware.FailedLoginMiddleware", ) -ROOT_URLCONF = 'defender.exampleapp.urls' +ROOT_URLCONF = "defender.exampleapp.urls" INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.admin', - 'django.contrib.staticfiles', - 'defender', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.admin", + "django.contrib.staticfiles", + "defender", ] # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) -SECRET_KEY = os.environ.get('SECRET_KEY', 'too-secret-for-test') +SECRET_KEY = os.environ.get("SECRET_KEY", "too-secret-for-test") -LOGIN_REDIRECT_URL = '/admin' +LOGIN_REDIRECT_URL = "/admin" DEFENDER_LOGIN_FAILURE_LIMIT = 1 DEFENDER_COOLOFF_TIME = 60 @@ -62,22 +61,22 @@ DEFENDER_REDIS_URL = "redis://localhost:6379/1" DEFENDER_MOCK_REDIS = False # Let's use custom function and strip username string from request. DEFENDER_GET_USERNAME_FROM_REQUEST_PATH = ( - 'defender.exampleapp.utils.strip_username_from_request' + "defender.exampleapp.utils.strip_username_from_request" ) # Celery settings: CELERY_ALWAYS_EAGER = True -BROKER_BACKEND = 'memory' -BROKER_URL = 'memory://' +BROKER_BACKEND = "memory" +BROKER_URL = "memory://" # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'defender.exampleapp.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "defender.exampleapp.settings") -app = Celery('defender') +app = Celery("defender") # Using a string here means the worker will not have to # pickle the object when using Windows. -app.config_from_object('django.conf:settings') +app.config_from_object("django.conf:settings") app.autodiscover_tasks(lambda: INSTALLED_APPS) DEBUG = True diff --git a/defender/exampleapp/urls.py b/defender/exampleapp/urls.py index c0d8596..1f367b6 100644 --- a/defender/exampleapp/urls.py +++ b/defender/exampleapp/urls.py @@ -7,9 +7,9 @@ from django.conf.urls.static import static admin.autodiscover() urlpatterns = patterns( - '', - (r'^admin/', include(admin.site.urls)), - (r'^admin/defender/', include('defender.urls')), + "", + (r"^admin/", include(admin.site.urls)), + (r"^admin/defender/", include("defender.urls")), ) diff --git a/defender/management/commands/cleanup_django_defender.py b/defender/management/commands/cleanup_django_defender.py index d0c506c..12af5d2 100644 --- a/defender/management/commands/cleanup_django_defender.py +++ b/defender/management/commands/cleanup_django_defender.py @@ -10,6 +10,7 @@ from ... import config class Command(BaseCommand): """ clean up management command """ + help = "Cleans up django-defender AccessAttempt table" def handle(self, **options): @@ -31,5 +32,6 @@ class Command(BaseCommand): print( "Finished. Removed {0} AccessAttempt entries.".format( - attempts_to_clean_count) + attempts_to_clean_count + ) ) diff --git a/defender/middleware.py b/defender/middleware.py index 45a6fa7..f6ef56b 100644 --- a/defender/middleware.py +++ b/defender/middleware.py @@ -10,6 +10,7 @@ from .decorators import watch_login class FailedLoginMiddleware(MIDDLEWARE_BASE_CLASS): """ Failed login middleware """ + patched = False def __init__(self, *args, **kwargs): @@ -22,6 +23,7 @@ class FailedLoginMiddleware(MIDDLEWARE_BASE_CLASS): # `LoginView` class-based view try: from django.contrib.auth.views import LoginView + our_decorator = watch_login() watch_login_method = method_decorator(our_decorator) LoginView.dispatch = watch_login_method(LoginView.dispatch) diff --git a/defender/migrations/0001_initial.py b/defender/migrations/0001_initial.py index 915bcc3..195a541 100644 --- a/defender/migrations/0001_initial.py +++ b/defender/migrations/0001_initial.py @@ -7,25 +7,36 @@ from django.db import models, migrations class Migration(migrations.Migration): """ Initial migrations """ - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='AccessAttempt', + name="AccessAttempt", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('user_agent', models.CharField(max_length=255)), - ('ip_address', models.GenericIPAddressField(null=True, verbose_name='IP Address')), - ('username', models.CharField(max_length=255, null=True)), - ('http_accept', models.CharField(max_length=1025, verbose_name='HTTP Accept')), - ('path_info', models.CharField(max_length=255, verbose_name='Path')), - ('attempt_time', models.DateTimeField(auto_now_add=True)), - ('login_valid', models.BooleanField(default=False)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("user_agent", models.CharField(max_length=255)), + ( + "ip_address", + models.GenericIPAddressField(null=True, verbose_name="IP Address"), + ), + ("username", models.CharField(max_length=255, null=True)), + ( + "http_accept", + models.CharField(max_length=1025, verbose_name="HTTP Accept"), + ), + ("path_info", models.CharField(max_length=255, verbose_name="Path")), + ("attempt_time", models.DateTimeField(auto_now_add=True)), + ("login_valid", models.BooleanField(default=False)), ], - options={ - 'ordering': ['-attempt_time'], - }, + options={"ordering": ["-attempt_time"],}, bases=(models.Model,), ), ] diff --git a/defender/models.py b/defender/models.py index 868f77e..82450f4 100644 --- a/defender/models.py +++ b/defender/models.py @@ -7,37 +7,20 @@ from django.utils.encoding import python_2_unicode_compatible @python_2_unicode_compatible class AccessAttempt(models.Model): """ Access Attempt log """ - user_agent = models.CharField( - max_length=255, - ) - ip_address = models.GenericIPAddressField( - verbose_name='IP Address', - null=True, - ) - username = models.CharField( - max_length=255, - null=True, - ) - http_accept = models.CharField( - verbose_name='HTTP Accept', - max_length=1025, - ) - path_info = models.CharField( - verbose_name='Path', - max_length=255, - ) - attempt_time = models.DateTimeField( - auto_now_add=True, - ) - login_valid = models.BooleanField( - default=False, - ) + + user_agent = models.CharField(max_length=255,) + ip_address = models.GenericIPAddressField(verbose_name="IP Address", null=True,) + username = models.CharField(max_length=255, null=True,) + http_accept = models.CharField(verbose_name="HTTP Accept", max_length=1025,) + path_info = models.CharField(verbose_name="Path", max_length=255,) + attempt_time = models.DateTimeField(auto_now_add=True,) + login_valid = models.BooleanField(default=False,) class Meta: - ordering = ['-attempt_time'] + ordering = ["-attempt_time"] def __str__(self): """ unicode value for this model """ - return "{0} @ {1} | {2}".format(self.username, - self.attempt_time, - self.login_valid) + return "{0} @ {1} | {2}".format( + self.username, self.attempt_time, self.login_valid + ) diff --git a/defender/signals.py b/defender/signals.py index c9cd9c1..a0674b2 100644 --- a/defender/signals.py +++ b/defender/signals.py @@ -1,9 +1,9 @@ from django.dispatch import Signal -username_block = Signal(providing_args=['username']) -username_unblock = Signal(providing_args=['username']) -ip_block = Signal(providing_args=['ip_address']) -ip_unblock = Signal(providing_args=['ip_address']) +username_block = Signal(providing_args=["username"]) +username_unblock = Signal(providing_args=["username"]) +ip_block = Signal(providing_args=["ip_address"]) +ip_unblock = Signal(providing_args=["ip_address"]) class BlockSignal: @@ -11,6 +11,7 @@ class BlockSignal: Providing a sender is mandatory when sending signals, hence this empty sender class. """ + pass diff --git a/defender/south_migrations/0001_initial.py b/defender/south_migrations/0001_initial.py index 1d17ee1..c60bad3 100644 --- a/defender/south_migrations/0001_initial.py +++ b/defender/south_migrations/0001_initial.py @@ -10,36 +10,92 @@ class Migration(SchemaMigration): def forwards(self, orm): """ Adding model 'AccessAttempt' """ - db.create_table(u'defender_accessattempt', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user_agent', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('ip_address', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39, null=True)), - ('username', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)), - ('http_accept', self.gf('django.db.models.fields.CharField')(max_length=1025)), - ('path_info', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('attempt_time', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('login_valid', self.gf('django.db.models.fields.BooleanField')(default=False)), - )) - db.send_create_signal(u'defender', ['AccessAttempt']) - + db.create_table( + "defender_accessattempt", + ( + ("id", self.gf("django.db.models.fields.AutoField")(primary_key=True)), + ( + "user_agent", + self.gf("django.db.models.fields.CharField")(max_length=255), + ), + ( + "ip_address", + self.gf("django.db.models.fields.GenericIPAddressField")( + max_length=39, null=True + ), + ), + ( + "username", + self.gf("django.db.models.fields.CharField")( + max_length=255, null=True + ), + ), + ( + "http_accept", + self.gf("django.db.models.fields.CharField")(max_length=1025), + ), + ( + "path_info", + self.gf("django.db.models.fields.CharField")(max_length=255), + ), + ( + "attempt_time", + self.gf("django.db.models.fields.DateTimeField")( + auto_now_add=True, blank=True + ), + ), + ( + "login_valid", + self.gf("django.db.models.fields.BooleanField")(default=False), + ), + ), + ) + db.send_create_signal("defender", ["AccessAttempt"]) def backwards(self, orm): # Deleting model 'AccessAttempt' - db.delete_table(u'defender_accessattempt') - + db.delete_table("defender_accessattempt") models = { - u'defender.accessattempt': { - 'Meta': {'ordering': "[u'-attempt_time']", 'object_name': 'AccessAttempt'}, - 'attempt_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'http_accept': ('django.db.models.fields.CharField', [], {'max_length': '1025'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True'}), - 'login_valid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'path_info': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + "defender.accessattempt": { + "Meta": {"ordering": "[u'-attempt_time']", "object_name": "AccessAttempt"}, + "attempt_time": ( + "django.db.models.fields.DateTimeField", + [], + {"auto_now_add": "True", "blank": "True"}, + ), + "http_accept": ( + "django.db.models.fields.CharField", + [], + {"max_length": "1025"}, + ), + "id": ("django.db.models.fields.AutoField", [], {"primary_key": "True"}), + "ip_address": ( + "django.db.models.fields.GenericIPAddressField", + [], + {"max_length": "39", "null": "True"}, + ), + "login_valid": ( + "django.db.models.fields.BooleanField", + [], + {"default": "False"}, + ), + "path_info": ( + "django.db.models.fields.CharField", + [], + {"max_length": "255"}, + ), + "user_agent": ( + "django.db.models.fields.CharField", + [], + {"max_length": "255"}, + ), + "username": ( + "django.db.models.fields.CharField", + [], + {"max_length": "255", "null": "True"}, + ), } } - complete_apps = ['defender'] + complete_apps = ["defender"] diff --git a/defender/tasks.py b/defender/tasks.py index b7d9ff9..41c416b 100644 --- a/defender/tasks.py +++ b/defender/tasks.py @@ -7,8 +7,10 @@ from celery import shared_task @shared_task() -def add_login_attempt_task(user_agent, ip_address, username, - http_accept, path_info, login_valid): +def add_login_attempt_task( + user_agent, ip_address, username, http_accept, path_info, login_valid +): """ Create a record for the login attempt """ - store_login_attempt(user_agent, ip_address, username, - http_accept, path_info, login_valid) + store_login_attempt( + user_agent, ip_address, username, http_accept, path_info, login_valid + ) diff --git a/defender/test.py b/defender/test.py index c72689d..b02f209 100644 --- a/defender/test.py +++ b/defender/test.py @@ -14,9 +14,11 @@ class DefenderTestCaseMixin(object): class DefenderTransactionTestCase(DefenderTestCaseMixin, TransactionTestCase): """Helper TransactionTestCase that cleans the cache after each test""" + pass class DefenderTestCase(DefenderTestCaseMixin, TestCase): """Helper TestCase that cleans the cache after each test""" + pass diff --git a/defender/test_settings.py b/defender/test_settings.py index 7e450a1..3371b10 100644 --- a/defender/test_settings.py +++ b/defender/test_settings.py @@ -1,56 +1,51 @@ import os from celery import Celery -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - } -} +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:",}} SITE_ID = 1 MIDDLEWARE = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'defender.middleware.FailedLoginMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "defender.middleware.FailedLoginMiddleware", ) -ROOT_URLCONF = 'defender.test_urls' +ROOT_URLCONF = "defender.test_urls" INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.admin', - 'defender', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.admin", + "defender", ] TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", ], }, }, ] -SECRET_KEY = os.environ.get('SECRET_KEY', 'too-secret-for-test') +SECRET_KEY = os.environ.get("SECRET_KEY", "too-secret-for-test") -LOGIN_REDIRECT_URL = '/admin' +LOGIN_REDIRECT_URL = "/admin" DEFENDER_LOGIN_FAILURE_LIMIT = 10 DEFENDER_COOLOFF_TIME = 2 @@ -60,15 +55,15 @@ DEFENDER_MOCK_REDIS = True # celery settings CELERY_ALWAYS_EAGER = True -BROKER_BACKEND = 'memory' -BROKER_URL = 'memory://' +BROKER_BACKEND = "memory" +BROKER_URL = "memory://" # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'defender.test_settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "defender.test_settings") -app = Celery('defender') +app = Celery("defender") # Using a string here means the worker will not have to # pickle the object when using Windows. -app.config_from_object('django.conf:settings') +app.config_from_object("django.conf:settings") app.autodiscover_tasks(lambda: INSTALLED_APPS) diff --git a/defender/test_urls.py b/defender/test_urls.py index 3d65a26..1776d41 100644 --- a/defender/test_urls.py +++ b/defender/test_urls.py @@ -3,6 +3,4 @@ from django.contrib import admin from .urls import urlpatterns as original_urlpatterns -urlpatterns = [ - url(r'^admin/', admin.site.urls), -] + original_urlpatterns +urlpatterns = [url(r"^admin/", admin.site.urls),] + original_urlpatterns diff --git a/defender/tests.py b/defender/tests.py index 99e41fc..9609f86 100644 --- a/defender/tests.py +++ b/defender/tests.py @@ -16,6 +16,7 @@ from django.contrib.sessions.backends.db import SessionStore from django.http import HttpRequest, HttpResponse from django.test.client import RequestFactory from redis.client import Redis + try: from django.urls import reverse except ImportError: @@ -35,30 +36,36 @@ from .models import AccessAttempt from .test import DefenderTestCase, DefenderTransactionTestCase LOGIN_FORM_KEY = '