mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 19:17:13 -05:00 
			
		
		
		
	Enhancement: re-implement remote user auth for API as opt-in (#5561)
This commit is contained in:
		
							parent
							
								
									38a817e887
								
							
						
					
					
						commit
						61209b1057
					
				@ -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 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
 | 
			
		||||
 | 
			
		||||
@ -462,9 +462,21 @@ applications.
 | 
			
		||||
 | 
			
		||||
    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).
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -420,19 +420,34 @@ 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(
 | 
			
		||||
 | 
			
		||||
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",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										110
									
								
								src/paperless/tests/test_remote_user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/paperless/tests/test_remote_user.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,110 @@
 | 
			
		||||
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")
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user