Merge branch 'dev' into feature-better-bs-icons
This commit is contained in:
commit
3538f362a3
@ -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
|
||||
|
@ -3,8 +3,8 @@
|
||||
i18n-title
|
||||
info="Review the log files for the application and for email checking."
|
||||
i18n-info>
|
||||
<div class="form-check form-switch" (click)="toggleAutoRefresh()">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
||||
<div class="form-check form-switch">
|
||||
<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>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
<i-bs name="check2-all"></i-bs> {{dismissButtonText}}
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -71,8 +71,8 @@
|
||||
|
||||
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
||||
|
||||
<div class="btn-toolbar mb-1 pb-3 border-bottom">
|
||||
<div class="btn-group">
|
||||
<div class="btn-toolbar mb-1 border-bottom">
|
||||
<div class="btn-group pb-3">
|
||||
<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>
|
||||
</button>
|
||||
@ -310,7 +310,7 @@
|
||||
</div>
|
||||
|
||||
<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 }">
|
||||
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
|
||||
@if (hasNext()) {
|
||||
|
@ -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 ||
|
||||
|
@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
|
@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
|
@ -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,18 +420,30 @@ 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")
|
||||
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",
|
||||
)
|
||||
|
||||
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"
|
||||
|
75
src/paperless/tests/test_remote_user.py
Normal file
75
src/paperless/tests/test_remote_user.py
Normal 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")
|
Loading…
x
Reference in New Issue
Block a user