Merge pull request #2359 from paperless-ngx/feature-log-failed-auth

Feature: Log failed login attempts
This commit is contained in:
shamoon
2023-02-18 19:40:51 -08:00
committed by GitHub
7 changed files with 153 additions and 1 deletions

15
src/paperless/apps.py Normal file
View File

@@ -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)

View 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.

32
src/paperless/signals.py Normal file
View File

@@ -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}.`",
)

View File

@@ -0,0 +1,80 @@
from django.http import HttpRequest
from django.test import TestCase
from paperless.signals import handle_failed_login
class TestFailedLoginLogging(TestCase):
def setUp(self):
super().setUp()
self.creds = {
"username": "john lennon",
}
def test_none(self):
"""
GIVEN:
- Request with no IP possible
WHEN:
- Request provided to signal handler
THEN:
- Unable to determine logged
"""
request = HttpRequest()
request.META = {}
with self.assertLogs("paperless.auth") as logs:
handle_failed_login(None, self.creds, request)
self.assertEqual(
logs.output,
[
"INFO:paperless.auth:Login failed for user `john lennon`. Unable to determine IP address.",
],
)
def test_public(self):
"""
GIVEN:
- Request with publicly routeable IP
WHEN:
- Request provided to signal handler
THEN:
- Expected IP is logged
"""
request = HttpRequest()
request.META = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139",
}
with self.assertLogs("paperless.auth") as logs:
handle_failed_login(None, self.creds, request)
self.assertEqual(
logs.output,
[
"INFO:paperless.auth:Login failed for user `john lennon` from IP `177.139.233.139.`",
],
)
def test_private(self):
"""
GIVEN:
- Request with private range IP
WHEN:
- Request provided to signal handler
THEN:
- Expected IP is logged
- IP is noted to be a private IP
"""
request = HttpRequest()
request.META = {
"HTTP_X_FORWARDED_FOR": "10.0.0.1",
}
with self.assertLogs("paperless.auth") as logs:
handle_failed_login(None, self.creds, request)
self.assertEqual(
logs.output,
[
"INFO:paperless.auth:Login failed for user `john lennon` from private IP `10.0.0.1.`",
],
)