mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-15 18:36:42 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13ffed0702 |
No files matched your search
@@ -1940,10 +1940,6 @@
|
|||||||
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">45</context>
|
<context context-type="linenumber">45</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts</context>
|
|
||||||
<context context-type="linenumber">59</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3066660568529853846" datatype="html">
|
<trans-unit id="3066660568529853846" datatype="html">
|
||||||
<source>Error retrieving groups</source>
|
<source>Error retrieving groups</source>
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
</pngx-input-select>
|
</pngx-input-select>
|
||||||
<pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check>
|
||||||
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check>
|
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check>
|
||||||
<pngx-permissions-form [users]="users()" accordion="true" formControlName="permissions_form"></pngx-permissions-form>
|
<pngx-permissions-form accordion="true" formControlName="permissions_form"></pngx-permissions-form>
|
||||||
@if (error()?.filter_rules) {
|
@if (error()?.filter_rules) {
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
|
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
|
||||||
|
|||||||
+1
-16
@@ -3,7 +3,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { User } from 'src/app/data/user'
|
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { CheckComponent } from '../../common/input/check/check.component'
|
import { CheckComponent } from '../../common/input/check/check.component'
|
||||||
@@ -15,7 +14,6 @@ import { TextComponent } from '../../common/input/text/text.component'
|
|||||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog.component'
|
import { SaveViewConfigDialogComponent } from './save-view-config-dialog.component'
|
||||||
|
|
||||||
describe('SaveViewConfigDialogComponent', () => {
|
describe('SaveViewConfigDialogComponent', () => {
|
||||||
const users = [{ id: 10, username: 'user10' }] as User[]
|
|
||||||
let component: SaveViewConfigDialogComponent
|
let component: SaveViewConfigDialogComponent
|
||||||
let fixture: ComponentFixture<SaveViewConfigDialogComponent>
|
let fixture: ComponentFixture<SaveViewConfigDialogComponent>
|
||||||
let modal: NgbActiveModal
|
let modal: NgbActiveModal
|
||||||
@@ -27,7 +25,7 @@ describe('SaveViewConfigDialogComponent', () => {
|
|||||||
{
|
{
|
||||||
provide: UserService,
|
provide: UserService,
|
||||||
useValue: {
|
useValue: {
|
||||||
listAll: () => of({ results: users }),
|
listAll: () => of({ results: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -126,19 +124,6 @@ describe('SaveViewConfigDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should list users in the owner selector', () => {
|
|
||||||
fixture.debugElement
|
|
||||||
.query(By.css('button[ngbAccordionButton]'))
|
|
||||||
.nativeElement.click()
|
|
||||||
fixture.detectChanges()
|
|
||||||
|
|
||||||
const permissionsForm = fixture.debugElement.query(
|
|
||||||
By.directive(PermissionsFormComponent)
|
|
||||||
)
|
|
||||||
const ownerSelect = permissionsForm.query(By.directive(SelectComponent))
|
|
||||||
expect(ownerSelect.componentInstance.items).toEqual(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support default name', () => {
|
it('should support default name', () => {
|
||||||
const saveClickedSpy = jest.spyOn(component.saveClicked, 'emit')
|
const saveClickedSpy = jest.spyOn(component.saveClicked, 'emit')
|
||||||
const modalCloseSpy = jest.spyOn(modal, 'close')
|
const modalCloseSpy = jest.spyOn(modal, 'close')
|
||||||
|
|||||||
+1
-16
@@ -6,7 +6,6 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
@@ -14,14 +13,11 @@ import {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { catchError, map, of } from 'rxjs'
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SAVED_VIEW_ICON,
|
DEFAULT_SAVED_VIEW_ICON,
|
||||||
SAVED_VIEW_ICONS,
|
SAVED_VIEW_ICONS,
|
||||||
} from 'src/app/data/saved-view-icons'
|
} from 'src/app/data/saved-view-icons'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { CheckComponent } from '../../common/input/check/check.component'
|
import { CheckComponent } from '../../common/input/check/check.component'
|
||||||
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
@@ -42,8 +38,6 @@ import { TextComponent } from '../../common/input/text/text.component'
|
|||||||
})
|
})
|
||||||
export class SaveViewConfigDialogComponent implements OnInit {
|
export class SaveViewConfigDialogComponent implements OnInit {
|
||||||
private modal = inject(NgbActiveModal)
|
private modal = inject(NgbActiveModal)
|
||||||
private userService = inject(UserService)
|
|
||||||
private toastService = inject(ToastService)
|
|
||||||
readonly error = signal(undefined)
|
readonly error = signal(undefined)
|
||||||
readonly buttonsEnabled = signal(true)
|
readonly buttonsEnabled = signal(true)
|
||||||
readonly defaultName = signal('')
|
readonly defaultName = signal('')
|
||||||
@@ -52,16 +46,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
|||||||
@Output()
|
@Output()
|
||||||
public saveClicked = new EventEmitter()
|
public saveClicked = new EventEmitter()
|
||||||
|
|
||||||
readonly users = toSignal(
|
users: User[]
|
||||||
this.userService.listAll().pipe(
|
|
||||||
map((result) => result.results),
|
|
||||||
catchError((error) => {
|
|
||||||
this.toastService.showError($localize`Error retrieving users`, error)
|
|
||||||
return of([])
|
|
||||||
})
|
|
||||||
),
|
|
||||||
{ initialValue: undefined as User[] }
|
|
||||||
)
|
|
||||||
readonly savedViewIcons = SAVED_VIEW_ICONS
|
readonly savedViewIcons = SAVED_VIEW_ICONS
|
||||||
|
|
||||||
setDefaultName(value: string) {
|
setDefaultName(value: string) {
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
"es2022",
|
"es2022",
|
||||||
"dom"
|
"dom"
|
||||||
],
|
],
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false
|
||||||
"types": ["jest", "node"]
|
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
OBFUSCATED_PASSWORD_MASK = "*" * 10
|
||||||
|
|
||||||
|
|
||||||
|
def is_obfuscated_password(value: str) -> bool:
|
||||||
|
"""
|
||||||
|
True if the value is only mask characters (or empty), meaning a client echoed
|
||||||
|
the placeholder back rather than supplying a new secret
|
||||||
|
"""
|
||||||
|
return value.replace("*", "") == ""
|
||||||
|
|
||||||
|
|
||||||
|
class ObfuscatedPasswordField(serializers.CharField):
|
||||||
|
"""
|
||||||
|
Sends a constant mask string instead of the password in the clear
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_representation(self, value) -> str:
|
||||||
|
return OBFUSCATED_PASSWORD_MASK
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return data
|
||||||
@@ -20,18 +20,19 @@ from rest_framework import serializers
|
|||||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
|
|
||||||
from paperless.config import OCR_RESERVED_USER_ARGS
|
from paperless.config import OCR_RESERVED_USER_ARGS
|
||||||
|
from paperless.fields import ObfuscatedPasswordField
|
||||||
|
from paperless.fields import is_obfuscated_password
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
from paperless.network import validate_outbound_http_url
|
from paperless.network import validate_outbound_http_url
|
||||||
from paperless.validators import reject_dangerous_svg
|
from paperless.validators import reject_dangerous_svg
|
||||||
from paperless.validators import validate_raster_image
|
from paperless.validators import validate_raster_image
|
||||||
from paperless_mail.serialisers import ObfuscatedPasswordField
|
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.settings")
|
logger = logging.getLogger("paperless.settings")
|
||||||
|
|
||||||
|
|
||||||
class PasswordValidationMixin:
|
class PasswordValidationMixin:
|
||||||
def _has_real_password(self, value: str | None) -> bool:
|
def _has_real_password(self, value: str | None) -> bool:
|
||||||
return bool(value) and value.replace("*", "") != ""
|
return bool(value) and not is_obfuscated_password(value)
|
||||||
|
|
||||||
def validate_password(self, value: str) -> str:
|
def validate_password(self, value: str) -> str:
|
||||||
if not self._has_real_password(value):
|
if not self._has_real_password(value):
|
||||||
@@ -285,7 +286,7 @@ class ApplicationConfigurationSerializer(
|
|||||||
if data[field] == "":
|
if data[field] == "":
|
||||||
data[field] = None
|
data[field] = None
|
||||||
# Not a real value, don't overwrite the stored one
|
# Not a real value, don't overwrite the stored one
|
||||||
elif len(data[field].replace("*", "")) == 0:
|
elif is_obfuscated_password(data[field]):
|
||||||
del data[field]
|
del data[field]
|
||||||
return super().run_validation(data)
|
return super().run_validation(data)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from paperless.fields import OBFUSCATED_PASSWORD_MASK
|
||||||
|
from paperless.fields import ObfuscatedPasswordField
|
||||||
|
from paperless.fields import is_obfuscated_password
|
||||||
|
|
||||||
|
|
||||||
|
class TestObfuscatedPasswordField:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"secret",
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"short",
|
||||||
|
"exactly10!",
|
||||||
|
"a-much-longer-secret-that-should-not-leak-its-length",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_representation_is_constant_regardless_of_length(self, secret: str) -> None:
|
||||||
|
assert (
|
||||||
|
ObfuscatedPasswordField().to_representation(secret)
|
||||||
|
== OBFUSCATED_PASSWORD_MASK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsObfuscatedPassword:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("value", "expected"),
|
||||||
|
[
|
||||||
|
("", True),
|
||||||
|
("*", True),
|
||||||
|
("******", True),
|
||||||
|
(OBFUSCATED_PASSWORD_MASK, True),
|
||||||
|
("real-secret", False),
|
||||||
|
("***x***", False),
|
||||||
|
(" ", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_detects_placeholder(self, value: str, *, expected: bool) -> None:
|
||||||
|
assert is_obfuscated_password(value) is expected
|
||||||
@@ -41,6 +41,7 @@ from documents.models import PaperlessTask
|
|||||||
from documents.permissions import PaperlessModelPermissions
|
from documents.permissions import PaperlessModelPermissions
|
||||||
from documents.permissions import PaperlessObjectPermissions
|
from documents.permissions import PaperlessObjectPermissions
|
||||||
from documents.tasks import llmindex_index
|
from documents.tasks import llmindex_index
|
||||||
|
from paperless.fields import is_obfuscated_password
|
||||||
from paperless.filters import GroupFilterSet
|
from paperless.filters import GroupFilterSet
|
||||||
from paperless.filters import UserFilterSet
|
from paperless.filters import UserFilterSet
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
@@ -260,7 +261,7 @@ class ProfileView(GenericAPIView[Any]):
|
|||||||
user = self.request.user if hasattr(self.request, "user") else None
|
user = self.request.user if hasattr(self.request, "user") else None
|
||||||
|
|
||||||
password = serializer.validated_data.pop("password", None)
|
password = serializer.validated_data.pop("password", None)
|
||||||
if password and password.replace("*", ""):
|
if password and not is_obfuscated_password(password):
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|||||||
@@ -8,23 +8,13 @@ from documents.serialisers import CorrespondentField
|
|||||||
from documents.serialisers import DocumentTypeField
|
from documents.serialisers import DocumentTypeField
|
||||||
from documents.serialisers import OwnedObjectSerializer
|
from documents.serialisers import OwnedObjectSerializer
|
||||||
from documents.serialisers import TagsField
|
from documents.serialisers import TagsField
|
||||||
|
from paperless.fields import ObfuscatedPasswordField
|
||||||
|
from paperless.fields import is_obfuscated_password
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
from paperless_mail.models import ProcessedMail
|
from paperless_mail.models import ProcessedMail
|
||||||
|
|
||||||
|
|
||||||
class ObfuscatedPasswordField(serializers.CharField):
|
|
||||||
"""
|
|
||||||
Sends *** string instead of password in the clear
|
|
||||||
"""
|
|
||||||
|
|
||||||
def to_representation(self, value) -> str:
|
|
||||||
return "*" * max(10, len(value))
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class MailAccountSerializer(OwnedObjectSerializer):
|
class MailAccountSerializer(OwnedObjectSerializer):
|
||||||
password = ObfuscatedPasswordField()
|
password = ObfuscatedPasswordField()
|
||||||
imap_port = serializers.IntegerField(required=True, allow_null=False)
|
imap_port = serializers.IntegerField(required=True, allow_null=False)
|
||||||
@@ -50,9 +40,8 @@ class MailAccountSerializer(OwnedObjectSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
if (
|
if "password" in validated_data and is_obfuscated_password(
|
||||||
"password" in validated_data
|
validated_data["password"],
|
||||||
and len(validated_data.get("password").replace("*", "")) == 0
|
|
||||||
):
|
):
|
||||||
validated_data.pop("password")
|
validated_data.pop("password")
|
||||||
super().update(instance, validated_data)
|
super().update(instance, validated_data)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from documents.permissions import PaperlessObjectPermissions
|
|||||||
from documents.permissions import has_perms_owner_aware
|
from documents.permissions import has_perms_owner_aware
|
||||||
from documents.permissions import permitted_object_ids
|
from documents.permissions import permitted_object_ids
|
||||||
from documents.views import PassUserMixin
|
from documents.views import PassUserMixin
|
||||||
|
from paperless.fields import is_obfuscated_password
|
||||||
from paperless.views import StandardPagination
|
from paperless.views import StandardPagination
|
||||||
from paperless_mail.filters import ProcessedMailFilterSet
|
from paperless_mail.filters import ProcessedMailFilterSet
|
||||||
from paperless_mail.mail import MailError
|
from paperless_mail.mail import MailError
|
||||||
@@ -126,9 +127,8 @@ class MailAccountViewSet(PassUserMixin, ModelViewSet[MailAccount]):
|
|||||||
return HttpResponseForbidden("Insufficient permissions")
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
# account exists, use the password from there instead of ***
|
# account exists, use the password from there instead of ***
|
||||||
if (
|
if existing_account is not None and is_obfuscated_password(
|
||||||
len(serializer.validated_data.get("password").replace("*", "")) == 0
|
serializer.validated_data.get("password"),
|
||||||
and existing_account is not None
|
|
||||||
):
|
):
|
||||||
serializer.validated_data["password"] = existing_account.password
|
serializer.validated_data["password"] = existing_account.password
|
||||||
serializer.validated_data["account_type"] = existing_account.account_type
|
serializer.validated_data["account_type"] = existing_account.account_type
|
||||||
|
|||||||
Reference in new issue
Block a user