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
|
## 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
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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> {{dismissButtonText}}
|
<i-bs name="check2-all"></i-bs> {{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>
|
||||||
|
@ -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()) {
|
||||||
|
@ -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 ||
|
||||||
|
@ -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,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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,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(
|
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",
|
"PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME",
|
||||||
"HTTP_REMOTE_USER",
|
"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 for embedded PDF display:
|
||||||
X_FRAME_OPTIONS = "ANY" if DEBUG else "SAMEORIGIN"
|
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