]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Log failed login attempts
authorMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Sat, 31 Dec 2022 21:13:19 +0000 (13:13 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Fri, 17 Feb 2023 16:12:27 +0000 (08:12 -0800)
Pipfile
Pipfile.lock
docs/configuration.md
src/paperless/apps.py [new file with mode: 0644]
src/paperless/settings.py
src/paperless/signals.py [new file with mode: 0644]

diff --git a/Pipfile b/Pipfile
index 2684ef2dabce05688eaaa663c9744ad88f1e842d..0909f12b2139141e24fb0bd7b3070048ab38aef3 100644 (file)
--- a/Pipfile
+++ b/Pipfile
@@ -67,6 +67,7 @@ djangorestframework-guardian = "*"
 # Locked version until https://github.com/django/channels_redis/issues/332
 # is resolved
 channels-redis = "==3.4.1"
+django-ipware = "*"
 
 
 [dev-packages]
index efe0eabbb879364b5016bba4fa7f1818e5060733..14237a002e1a4616ba78b3b68926f66e7cc9a29b 100644 (file)
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "99f415c5ce96020dc3fcb137dc15d47cc5431686bdce1ca42e6254a2719060a8"
+            "sha256": "0e1a26c5e9acb1d745f951f92d00d60272f83406467d90551e558972697b53cd"
         },
         "pipfile-spec": 6,
         "requires": {},
             "index": "pypi",
             "version": "==2.4.0"
         },
+        "django-ipware": {
+            "hashes": [
+                "sha256:602a58325a4808bd19197fef2676a0b2da2df40d0ecf21be414b2ff48c72ad05",
+                "sha256:878dbb06a87e25550798e9ef3204ed70a200dd8b15e47dcef848cf08244f04c9"
+            ],
+            "index": "pypi",
+            "version": "==4.0.2"
+        },
         "djangorestframework": {
             "hashes": [
                 "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8",
index 5d4ed7a693085ec6240cc23fd42cb9d606c6c726..6c233c2e60c498cbcc7858e13da929bf57415cda 100644 (file)
@@ -262,6 +262,14 @@ do CORS calls. Set this to your public domain name.
 
     Defaults to "<http://localhost:8000>".
 
+`PAPERLESS_TRUSTED_PROXIES=<comma-separated-list>`
+
+: This may be needed to prevent IP address spoofing if you are using e.g.
+fail2ban with log entries for failed authorization attempts. Value should be
+IP address(es).
+
+    Defaults to empty string.
+
 `PAPERLESS_FORCE_SCRIPT_NAME=<path>`
 
 : To host paperless under a subpath url like example.com/paperless you
diff --git a/src/paperless/apps.py b/src/paperless/apps.py
new file mode 100644 (file)
index 0000000..3230997
--- /dev/null
@@ -0,0 +1,15 @@
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+from paperless.signals import handle_failed_login
+
+
+class PaperlessConfig(AppConfig):
+    name = "paperless"
+
+    verbose_name = _("Paperless")
+
+    def ready(self):
+        from django.contrib.auth.signals import user_login_failed
+
+        user_login_failed.connect(handle_failed_login)
+        AppConfig.ready(self)
index 7fc384eaa548cc4a0309f43e0723c5084c50d0df..41f08f3e2562a89b92e5b53c91f8f7a502dcda79 100644 (file)
@@ -416,6 +416,13 @@ if _paperless_url:
         # always allow localhost. Necessary e.g. for healthcheck in docker.
         ALLOWED_HOSTS = [_paperless_uri.hostname] + ["localhost"]
 
+# For use with trusted proxies
+_trusted_proxies = os.getenv("PAPERLESS_TRUSTED_PROXIES")
+if _trusted_proxies:
+    TRUSTED_PROXIES = _trusted_proxies.split(",")
+else:
+    TRUSTED_PROXIES = []
+
 # The secret key has a default that should be fine so long as you're hosting
 # Paperless on a closed network.  However, if you're putting this anywhere
 # public, you should change the key to something unique and verbose.
diff --git a/src/paperless/signals.py b/src/paperless/signals.py
new file mode 100644 (file)
index 0000000..cedad7f
--- /dev/null
@@ -0,0 +1,32 @@
+import logging
+
+from django.conf import settings
+from ipware import get_client_ip
+
+logger = logging.getLogger("paperless.auth")
+
+
+# https://docs.djangoproject.com/en/4.1/ref/contrib/auth/#django.contrib.auth.signals.user_login_failed
+def handle_failed_login(sender, credentials, request, **kwargs):
+    client_ip, is_routable = get_client_ip(
+        request,
+        proxy_trusted_ips=settings.TRUSTED_PROXIES,
+    )
+    if client_ip is None:
+        logger.info(
+            f"Login failed for user `{credentials['username']}`."
+            + " Unable to determine IP address.",
+        )
+    else:
+        if is_routable:
+            # We got the client's IP address
+            logger.info(
+                f"Login failed for user `{credentials['username']}`"
+                + f" from IP `{client_ip}.`",
+            )
+        else:
+            # The client's IP address is private
+            logger.info(
+                f"Login failed for user `{credentials['username']}`"
+                + f" from private IP `{client_ip}.`",
+            )