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-ui/src/app/components/admin/logs/logs.component.html b/src-ui/src/app/components/admin/logs/logs.component.html index b75f85ae5..9a0a71cd8 100644 --- a/src-ui/src/app/components/admin/logs/logs.component.html +++ b/src-ui/src/app/components/admin/logs/logs.component.html @@ -3,8 +3,8 @@ i18n-title info="Review the log files for the application and for email checking." i18n-info> -
- +
+
diff --git a/src-ui/src/app/components/admin/logs/logs.component.ts b/src-ui/src/app/components/admin/logs/logs.component.ts index 0c1231a48..1b2dbef3e 100644 --- a/src-ui/src/app/components/admin/logs/logs.component.ts +++ b/src-ui/src/app/components/admin/logs/logs.component.ts @@ -2,9 +2,9 @@ import { Component, ElementRef, OnInit, - AfterViewChecked, ViewChild, OnDestroy, + ChangeDetectorRef, } from '@angular/core' import { Subject, takeUntil } from 'rxjs' import { LogService } from 'src/app/services/rest/log.service' @@ -14,8 +14,11 @@ import { LogService } from 'src/app/services/rest/log.service' templateUrl: './logs.component.html', styleUrls: ['./logs.component.scss'], }) -export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy { - constructor(private logService: LogService) {} +export class LogsComponent implements OnInit, OnDestroy { + constructor( + private logService: LogService, + private changedetectorRef: ChangeDetectorRef + ) {} public logs: string[] = [] @@ -47,10 +50,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy { }) } - ngAfterViewChecked() { - this.scrollToBottom() - } - ngOnDestroy(): void { this.unsubscribeNotifier.next(true) this.unsubscribeNotifier.complete() @@ -66,6 +65,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy { next: (result) => { this.logs = result this.isLoading = false + this.scrollToBottom() }, error: () => { this.logs = [] @@ -89,6 +89,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy { } scrollToBottom(): void { + this.changedetectorRef.detectChanges() this.logContainer?.nativeElement.scroll({ top: this.logContainer.nativeElement.scrollHeight, left: 0, diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.html b/src-ui/src/app/components/admin/tasks/tasks.component.html index 0084d9cf6..dd5f84d06 100644 --- a/src-ui/src/app/components/admin/tasks/tasks.component.html +++ b/src-ui/src/app/components/admin/tasks/tasks.component.html @@ -11,8 +11,8 @@ -
- +
+
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 7435fac36..c44a202d0 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -71,8 +71,8 @@
-
-
+
+
@@ -310,7 +310,7 @@
-
+
@if (hasNext()) { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 0e79b3deb..798e867bf 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -861,8 +861,11 @@ export class DocumentDetailComponent get userIsOwner(): boolean { let doc: Document = Object.assign({}, this.document) // dont disable while editing - if (this.document && this.store?.value.permissions_form?.owner) { - doc.owner = this.store?.value.permissions_form?.owner + if ( + this.document && + this.store?.value.permissions_form?.hasOwnProperty('owner') + ) { + doc.owner = this.store.value.permissions_form.owner } return !this.document || this.permissionsService.currentUserOwnsObject(doc) } @@ -870,8 +873,11 @@ export class DocumentDetailComponent get userCanEdit(): boolean { let doc: Document = Object.assign({}, this.document) // dont disable while editing - if (this.document && this.store?.value.permissions_form?.owner) { - doc.owner = this.store?.value.permissions_form?.owner + if ( + this.document && + this.store?.value.permissions_form?.hasOwnProperty('owner') + ) { + doc.owner = this.store.value.permissions_form.owner } return ( !this.document || diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts index 412fe3cc1..9e8663383 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts @@ -11,6 +11,7 @@ import { NgbPaginationModule, NgbModalModule, NgbModalRef, + NgbPopoverModule, } from '@ng-bootstrap/ng-bootstrap' import { of, throwError } from 'rxjs' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' @@ -64,6 +65,7 @@ describe('CustomFieldsComponent', () => { FormsModule, ReactiveFormsModule, NgbModalModule, + NgbPopoverModule, ], }) diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts index 1abbd2c5a..adf174207 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts @@ -7,6 +7,7 @@ import { NgbPaginationModule, NgbModalRef, NgbModalModule, + NgbPopoverModule, } from '@ng-bootstrap/ng-bootstrap' import { of, throwError } from 'rxjs' import { Workflow } from 'src/app/data/workflow' @@ -99,6 +100,7 @@ describe('WorkflowsComponent', () => { FormsModule, ReactiveFormsModule, NgbModalModule, + NgbPopoverModule, ], }) 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")