From 6db9e292ba3fe5838a1d4e88cfdcaf5e73a2f591 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 16 Jan 2024 15:26:05 -0800
Subject: [PATCH] Enhancement: support remote user auth directly against API
 (DRF) (#5386)

---
 docs/api.md                             |  8 ++-
 src/paperless/auth.py                   |  8 +++
 src/paperless/settings.py               | 32 +++++++----
 src/paperless/tests/test_remote_user.py | 75 +++++++++++++++++++++++++
 4 files changed, 112 insertions(+), 11 deletions(-)
 create mode 100644 src/paperless/tests/test_remote_user.py

diff --git a/docs/api.md b/docs/api.md
index 97ccf4c3a..e103ae14a 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -139,7 +139,7 @@ document. Paperless only reports PDF metadata at this point.
 
 ## Authorization
 
-The REST api provides three different forms of authentication.
+The REST api provides four different forms of authentication.
 
 1.  Basic authentication
 
@@ -177,6 +177,12 @@ The REST api provides three different forms of authentication.
 
     Tokens can also be managed in the Django admin.
 
+4.  Remote User authentication
+
+    If already setup (see
+    [configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER)),
+    you can authenticate against the API using Remote User auth.
+
 ## Searching for documents
 
 Full text searching is available on the `/api/documents/` endpoint. Two
diff --git a/src/paperless/auth.py b/src/paperless/auth.py
index a23b01cb4..98e2a8b30 100644
--- a/src/paperless/auth.py
+++ b/src/paperless/auth.py
@@ -47,3 +47,11 @@ class HttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
     """
 
     header = settings.HTTP_REMOTE_USER_HEADER_NAME
+
+
+class PaperlessRemoteUserAuthentication(authentication.RemoteUserAuthentication):
+    """
+    REMOTE_USER authentication for DRF which overrides the default header.
+    """
+
+    header = settings.HTTP_REMOTE_USER_HEADER_NAME
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
index bc815d4d5..54779006d 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -420,19 +420,31 @@ if AUTO_LOGIN_USERNAME:
     # regular login in case the provided user does not exist.
     MIDDLEWARE.insert(_index + 1, "paperless.auth.AutoLoginMiddleware")
 
-ENABLE_HTTP_REMOTE_USER = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER")
-HTTP_REMOTE_USER_HEADER_NAME = os.getenv(
-    "PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME",
-    "HTTP_REMOTE_USER",
-)
 
-if ENABLE_HTTP_REMOTE_USER:
-    MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware")
-    AUTHENTICATION_BACKENDS.insert(0, "django.contrib.auth.backends.RemoteUserBackend")
-    REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
-        "rest_framework.authentication.RemoteUserAuthentication",
+def _parse_remote_user_settings() -> str:
+    global MIDDLEWARE, AUTHENTICATION_BACKENDS, REST_FRAMEWORK
+    enable = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER")
+    if enable:
+        MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware")
+        AUTHENTICATION_BACKENDS.insert(
+            0,
+            "django.contrib.auth.backends.RemoteUserBackend",
+        )
+        REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].insert(
+            0,
+            "paperless.auth.PaperlessRemoteUserAuthentication",
+        )
+
+    header_name = os.getenv(
+        "PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME",
+        "HTTP_REMOTE_USER",
     )
 
+    return header_name
+
+
+HTTP_REMOTE_USER_HEADER_NAME = _parse_remote_user_settings()
+
 # X-Frame options for embedded PDF display:
 X_FRAME_OPTIONS = "ANY" if DEBUG else "SAMEORIGIN"
 
diff --git a/src/paperless/tests/test_remote_user.py b/src/paperless/tests/test_remote_user.py
new file mode 100644
index 000000000..194026e4d
--- /dev/null
+++ b/src/paperless/tests/test_remote_user.py
@@ -0,0 +1,75 @@
+import os
+from unittest import mock
+
+from django.contrib.auth.models import User
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.tests.utils import DirectoriesMixin
+from paperless.settings import _parse_remote_user_settings
+
+
+class TestRemoteUser(DirectoriesMixin, APITestCase):
+    def setUp(self):
+        super().setUp()
+
+        self.user = User.objects.create_superuser(
+            username="temp_admin",
+        )
+
+    def test_remote_user(self):
+        """
+        GIVEN:
+            - Configured user
+            - Remote user auth is enabled
+        WHEN:
+            - API call is made to get documents
+        THEN:
+            - Call succeeds
+        """
+
+        with mock.patch.dict(
+            os.environ,
+            {
+                "PAPERLESS_ENABLE_HTTP_REMOTE_USER": "True",
+            },
+        ):
+            _parse_remote_user_settings()
+
+            response = self.client.get("/api/documents/")
+
+            # 403 testing locally, 401 on ci...
+            self.assertIn(
+                response.status_code,
+                [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN],
+            )
+
+            response = self.client.get(
+                "/api/documents/",
+                headers={
+                    "Remote-User": self.user.username,
+                },
+            )
+
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_remote_user_header_setting(self):
+        """
+        GIVEN:
+            - Remote user header name is set
+        WHEN:
+            - Settings are parsed
+        THEN:
+            - Correct header name is returned
+        """
+
+        with mock.patch.dict(
+            os.environ,
+            {
+                "PAPERLESS_ENABLE_HTTP_REMOTE_USER": "True",
+                "PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME": "HTTP_FOO",
+            },
+        ):
+            header_name = _parse_remote_user_settings()
+
+            self.assertEqual(header_name, "HTTP_FOO")