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
 | 
					## Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The REST api provides three different forms of authentication.
 | 
					The REST api provides four different forms of authentication.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1.  Basic 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.
 | 
					    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
 | 
					## Searching for documents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Full text searching is available on the `/api/documents/` endpoint. Two
 | 
					Full text searching is available on the `/api/documents/` endpoint. Two
 | 
				
			||||||
 | 
				
			|||||||
@ -462,9 +462,21 @@ applications.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Defaults to "false" which disables this feature.
 | 
					    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}
 | 
					#### [`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
 | 
					property allows to customize the name of the HTTP header from which
 | 
				
			||||||
the authenticated username is extracted. Values are in terms of
 | 
					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).
 | 
					[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
 | 
					    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.
 | 
					    # regular login in case the provided user does not exist.
 | 
				
			||||||
    MIDDLEWARE.insert(_index + 1, "paperless.auth.AutoLoginMiddleware")
 | 
					    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:
 | 
					def _parse_remote_user_settings() -> str:
 | 
				
			||||||
    MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware")
 | 
					    global MIDDLEWARE, AUTHENTICATION_BACKENDS, REST_FRAMEWORK
 | 
				
			||||||
    AUTHENTICATION_BACKENDS.insert(0, "django.contrib.auth.backends.RemoteUserBackend")
 | 
					    enable = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER")
 | 
				
			||||||
    REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
 | 
					    enable_api = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER_API")
 | 
				
			||||||
        "rest_framework.authentication.RemoteUserAuthentication",
 | 
					    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 for embedded PDF display:
 | 
				
			||||||
X_FRAME_OPTIONS = "ANY" if DEBUG else "SAMEORIGIN"
 | 
					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