## Authorization
-The REST api provides three different forms of authentication.
+The REST api provides four different forms of authentication.
1. Basic authentication
Tokens can also be managed in the Django admin.
+4. Remote User authentication
+
+ If enabled (see
+ [configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API)),
+ you can authenticate against the API using Remote User auth.
+
## Searching for documents
Full text searching is available on the `/api/documents/` endpoint. Two
Defaults to "false" which disables this feature.
+#### [`PAPERLESS_ENABLE_HTTP_REMOTE_USER_API=<bool>`](#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API) {#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API}
+
+: Allows authentication via HTTP_REMOTE_USER directly against the API
+
+ !!! warning
+
+ See the warning above about securing your installation when using remote user header authentication. This setting is separate from
+ `PAPERLESS_ENABLE_HTTP_REMOTE_USER` to avoid introducing a security vulnerability to existing reverse proxy setups. As above,
+ ensure that your reverse proxy does not simply pass the `Remote-User` header from the internet to paperless.
+
+ Defaults to "false" which disables this feature.
+
#### [`PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=<str>`](#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME) {#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME}
-: If "PAPERLESS_ENABLE_HTTP_REMOTE_USER" is enabled, this
+: If "PAPERLESS_ENABLE_HTTP_REMOTE_USER" or `PAPERLESS_ENABLE_HTTP_REMOTE_USER_API` are enabled, this
property allows to customize the name of the HTTP header from which
the authenticated username is extracted. Values are in terms of
[HttpRequest.META](https://docs.djangoproject.com/en/4.1/ref/request-response/#django.http.HttpRequest.META).
"""
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
# 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")
+ enable_api = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER_API")
+ if enable or enable_api:
+ MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware")
+ AUTHENTICATION_BACKENDS.insert(
+ 0,
+ "django.contrib.auth.backends.RemoteUserBackend",
+ )
+
+ if enable_api:
+ 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"
--- /dev/null
+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:
+ - Call is made to root
+ THEN:
+ - Call succeeds
+ """
+
+ with mock.patch.dict(
+ os.environ,
+ {
+ "PAPERLESS_ENABLE_HTTP_REMOTE_USER": "True",
+ },
+ ):
+ _parse_remote_user_settings()
+
+ response = self.client.get("/documents/")
+
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_302_FOUND,
+ )
+
+ response = self.client.get(
+ "/documents/",
+ headers={
+ "Remote-User": self.user.username,
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_remote_user_api(self):
+ """
+ GIVEN:
+ - Configured user
+ - Remote user auth is enabled for the API
+ WHEN:
+ - API call is made to get documents
+ THEN:
+ - Call succeeds
+ """
+
+ with mock.patch.dict(
+ os.environ,
+ {
+ "PAPERLESS_ENABLE_HTTP_REMOTE_USER_API": "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")