Repository: carpentries/amy

Issue:
Log failed login attempts
The purpose of all this is to catch multiple login attempts and log the messages to the error log file. Then run `fail2ban` software on the log output so that it blocks the wrong-doing IP.

Handle Django's auth [`user_login_failed`](https://docs.djangoproject.com/en/2.1/ref/contrib/auth/#django.contrib.auth.signals.user_login_failed) signal.

Log the error message to the error log file.

Similar solution is [here](https://tech.marksblogg.com/django-admin-logins.html), although it doesn't use the newest version of `user_login_failed`.



Discussion:

Resolving patch:
diff --git a/amy/workshops/signals.py b/amy/workshops/signals.py
--- a/amy/workshops/signals.py
+++ b/amy/workshops/signals.py
@@ -1,7 +1,26 @@
-from django.dispatch import Signal
+import logging
 
+from django.contrib.auth.signals import user_login_failed
+from django.dispatch import Signal, receiver
+from django.http.request import HttpRequest
+
+
+# AMY server logger
+logger = logging.getLogger("amy.server_logs")
 
 # signal generated when a comment regarding specific object should be saved
 create_comment_signal = Signal(
     providing_args=["content_object", "comment", "timestamp"]
 )
+
+
+# a receiver for "failed login attempt" signal
+@receiver(user_login_failed)
+def log_user_login_failed(sender, **kwargs):
+    credentials = kwargs.get('credentials') or dict()
+    request = kwargs.get('request') or HttpRequest()
+
+    ip = request.META.get('REMOTE_ADDR') or 'UNKNOWN'
+    username = credentials.get('username') or 'UNKNOWN'
+    msg = "Login failure from IP {} for user '{}'".format(ip, username)
+    logger.error(msg)
diff --git a/config/settings.py b/config/settings.py
--- a/config/settings.py
+++ b/config/settings.py
@@ -460,10 +460,33 @@
 LOGGING = {
     'version': 1,
     'disable_existing_loggers': False,  # merge with default configuration
+    'formatters': {
+        'verbose': {
+            'format': '{asctime}::{levelname}::{message}',
+            'style': '{',
+        },
+        'simple': {
+            'format': '{levelname}::{message}',
+            'style': '{',
+        },
+    },
     'handlers': {
         'null': {
             'class': 'logging.NullHandler',
         },
+        'mail_admins': {
+            'level': 'ERROR',
+            'class': 'django.utils.log.AdminEmailHandler',
+            'email_backend': EMAIL_BACKEND,
+            'include_html': True,
+        },
+        'log_file': {
+            'level': 'ERROR',
+            'class': 'logging.FileHandler',
+            'formatter': 'verbose',
+            # `str()` prevents some strange bug on Py3.5
+            'filename': str(env.path('AMY_SERVER_LOGFILE', default='amy.log')),
+        },
     },
     'loggers': {
         # disable "Invalid HTTP_HOST" notifications
@@ -471,6 +494,15 @@
             'handlers': ['null'],
             'propagate': False,
         },
+        'amy': {
+            'handlers': ['null', ],
+            'level': 'WARNING',
+        },
+        'amy.server_logs': {
+            'handlers': ['log_file', ],
+            'level': 'ERROR',
+            'propagate': True,
+        },
     },
 }
 


Test patch: