mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-14 01:55:25 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13ffed0702 |
No files matched your search
@@ -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