Merge branch 'dev' into feature-better-bs-icons

This commit is contained in:
shamoon 2024-01-17 16:19:41 -08:00 committed by GitHub
commit 3538f362a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 141 additions and 29 deletions

View File

@ -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 already setup (see
[configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER)),
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

View File

@ -3,8 +3,8 @@
i18n-title i18n-title
info="Review the log files for the application and for email checking." info="Review the log files for the application and for email checking."
i18n-info> i18n-info>
<div class="form-check form-switch" (click)="toggleAutoRefresh()"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval"> <input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label> <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div> </div>
</pngx-page-header> </pngx-page-header>

View File

@ -2,9 +2,9 @@ import {
Component, Component,
ElementRef, ElementRef,
OnInit, OnInit,
AfterViewChecked,
ViewChild, ViewChild,
OnDestroy, OnDestroy,
ChangeDetectorRef,
} from '@angular/core' } from '@angular/core'
import { Subject, takeUntil } from 'rxjs' import { Subject, takeUntil } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service' 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', templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'], styleUrls: ['./logs.component.scss'],
}) })
export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy { export class LogsComponent implements OnInit, OnDestroy {
constructor(private logService: LogService) {} constructor(
private logService: LogService,
private changedetectorRef: ChangeDetectorRef
) {}
public logs: string[] = [] public logs: string[] = []
@ -47,10 +50,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
}) })
} }
ngAfterViewChecked() {
this.scrollToBottom()
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.unsubscribeNotifier.next(true) this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete() this.unsubscribeNotifier.complete()
@ -66,6 +65,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
next: (result) => { next: (result) => {
this.logs = result this.logs = result
this.isLoading = false this.isLoading = false
this.scrollToBottom()
}, },
error: () => { error: () => {
this.logs = [] this.logs = []
@ -89,6 +89,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
} }
scrollToBottom(): void { scrollToBottom(): void {
this.changedetectorRef.detectChanges()
this.logContainer?.nativeElement.scroll({ this.logContainer?.nativeElement.scroll({
top: this.logContainer.nativeElement.scrollHeight, top: this.logContainer.nativeElement.scrollHeight,
left: 0, left: 0,

View File

@ -11,8 +11,8 @@
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0"> <button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<i-bs name="check2-all"></i-bs>&nbsp;{{dismissButtonText}} <i-bs name="check2-all"></i-bs>&nbsp;{{dismissButtonText}}
</button> </button>
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()"> <div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval"> <input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label> <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div> </div>
</div> </div>

View File

@ -71,8 +71,8 @@
<form [formGroup]='documentForm' (ngSubmit)="save()"> <form [formGroup]='documentForm' (ngSubmit)="save()">
<div class="btn-toolbar mb-1 pb-3 border-bottom"> <div class="btn-toolbar mb-1 border-bottom">
<div class="btn-group"> <div class="btn-group pb-3">
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()"> <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()">
<i-bs width="1.2em" height="1.2em" name="x"></i-bs> <i-bs width="1.2em" height="1.2em" name="x"></i-bs>
</button> </button>
@ -310,7 +310,7 @@
</div> </div>
<ng-template #saveButtons> <ng-template #saveButtons>
<div class="btn-group ms-auto"> <div class="btn-group pb-3 ms-auto">
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button> <button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
@if (hasNext()) { @if (hasNext()) {

View File

@ -861,8 +861,11 @@ export class DocumentDetailComponent
get userIsOwner(): boolean { get userIsOwner(): boolean {
let doc: Document = Object.assign({}, this.document) let doc: Document = Object.assign({}, this.document)
// dont disable while editing // dont disable while editing
if (this.document && this.store?.value.permissions_form?.owner) { if (
doc.owner = this.store?.value.permissions_form?.owner this.document &&
this.store?.value.permissions_form?.hasOwnProperty('owner')
) {
doc.owner = this.store.value.permissions_form.owner
} }
return !this.document || this.permissionsService.currentUserOwnsObject(doc) return !this.document || this.permissionsService.currentUserOwnsObject(doc)
} }
@ -870,8 +873,11 @@ export class DocumentDetailComponent
get userCanEdit(): boolean { get userCanEdit(): boolean {
let doc: Document = Object.assign({}, this.document) let doc: Document = Object.assign({}, this.document)
// dont disable while editing // dont disable while editing
if (this.document && this.store?.value.permissions_form?.owner) { if (
doc.owner = this.store?.value.permissions_form?.owner this.document &&
this.store?.value.permissions_form?.hasOwnProperty('owner')
) {
doc.owner = this.store.value.permissions_form.owner
} }
return ( return (
!this.document || !this.document ||

View File

@ -11,6 +11,7 @@ import {
NgbPaginationModule, NgbPaginationModule,
NgbModalModule, NgbModalModule,
NgbModalRef, NgbModalRef,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@ -64,6 +65,7 @@ describe('CustomFieldsComponent', () => {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgbModalModule, NgbModalModule,
NgbPopoverModule,
], ],
}) })

View File

@ -7,6 +7,7 @@ import {
NgbPaginationModule, NgbPaginationModule,
NgbModalRef, NgbModalRef,
NgbModalModule, NgbModalModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { Workflow } from 'src/app/data/workflow' import { Workflow } from 'src/app/data/workflow'
@ -99,6 +100,7 @@ describe('WorkflowsComponent', () => {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgbModalModule, NgbModalModule,
NgbPopoverModule,
], ],
}) })

View File

@ -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

View File

@ -420,19 +420,31 @@ 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( if enable:
"rest_framework.authentication.RemoteUserAuthentication", 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 for embedded PDF display:
X_FRAME_OPTIONS = "ANY" if DEBUG else "SAMEORIGIN" X_FRAME_OPTIONS = "ANY" if DEBUG else "SAMEORIGIN"

View File

@ -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")