Compare commits

..
9 changed files with 105 additions and 8 deletions

No files matched your search

+4 -4
View File
@@ -38,7 +38,7 @@ repos:
- json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.9.10'
rev: 'v3.9.9'
hooks:
- id: prettier
types_or:
@@ -46,16 +46,16 @@ repos:
- ts
- markdown
additional_dependencies:
- prettier@3.9.10
- prettier@3.9.9
- 'prettier-plugin-organize-imports@4.3.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.17.2
rev: v0.16.9
hooks:
- id: ruff-check
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.30.3"
rev: "v2.29.4"
hooks:
- id: pyproject-fmt
additional_dependencies: [tomli]
+3
View File
@@ -1157,6 +1157,9 @@ they use underscores instead of dashes.
be changed by trusted users. This applies to the `AppConfig` permission as well,
which allows setting these arguments from the UI.
Arguments paperless manages itself (`input_file_or_options`, `output_file`
and `sidecar`) are rejected by the API and ignored if set here.
Specify arguments as a JSON dictionary. Keep note of lower case
booleans and double quoted parameter names and strings. Examples:
+6 -1
View File
@@ -212,7 +212,12 @@ extend-select = [
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
]
ignore = [ "DJ001", "ICN001", "PLC0415", "RUF012", "SIM105" ]
ignore = [
"DJ001",
"PLC0415",
"RUF012",
"SIM105",
]
# Migrations
per-file-ignores."*/migrations/*.py" = [
"E501",
+6 -2
View File
@@ -631,7 +631,9 @@ class ConsumerPlugin(
pk=self.metadata.actor_id,
).first()
if actor is not None:
from auditlog.context import set_actor # type: ignore[import-untyped]
from auditlog.context import ( # type: ignore[import-untyped]
set_actor,
)
with set_actor(actor):
original_document.save()
@@ -649,7 +651,9 @@ class ConsumerPlugin(
# Create a log entry for the version addition, if enabled
if settings.AUDIT_LOG_ENABLED:
from auditlog.models import LogEntry # type: ignore[import-untyped]
from auditlog.models import ( # type: ignore[import-untyped]
LogEntry,
)
LogEntry.objects.log_create(
instance=root_doc,
@@ -287,6 +287,28 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
self.assertEqual(config.user_args, user_args)
self.assertEqual(config.barcode_tag_mapping, barcode_tag_mapping)
def test_api_update_config_rejects_reserved_user_args(self) -> None:
"""
GIVEN:
- API request to update app config with user_args which set an
argument the parser manages itself
WHEN:
- API is called
THEN:
- Request is rejected with a 400 naming the argument
- Config is not updated
"""
response = self.client.patch(
f"{self.ENDPOINT}1/",
json.dumps({"user_args": json.dumps({"sidecar": "/tmp/x.py"})}),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("sidecar", str(response.data["user_args"]))
config = ApplicationConfiguration.objects.first()
assert config is not None
self.assertIsNone(config.user_args)
def test_api_update_config_rejects_invalid_json_objects(self) -> None:
"""
GIVEN:
+11
View File
@@ -12,6 +12,17 @@ from paperless.models import ModeChoices
from paperless.models import OutputTypeChoices
from paperless.models import RemoteOCRMode
# OCRmyPDF arguments the parser sets itself, pointing into its own temporary
# directory. The parser reads these files back after OCR, so user supplied
# values can only break consumption (or write to arbitrary paths), never help.
OCR_RESERVED_USER_ARGS: frozenset[str] = frozenset(
{
"input_file_or_options",
"output_file",
"sidecar",
},
)
@dataclasses.dataclass
class BaseConfig:
+17 -1
View File
@@ -21,6 +21,7 @@ from documents.parsers import make_thumbnail_from_pdf
from documents.utils import copy_file_with_basic_stats
from documents.utils import maybe_override_pixel_limit
from documents.utils import remove_alpha_channel
from paperless.config import OCR_RESERVED_USER_ARGS
from paperless.config import OcrConfig
from paperless.models import CleanChoices
from paperless.models import ModeChoices
@@ -351,7 +352,22 @@ class RasterisedDocumentParser:
if self.settings.user_args is not None:
try:
ocrmypdf_args = {**ocrmypdf_args, **self.settings.user_args}
reserved = sorted(
OCR_RESERVED_USER_ARGS & self.settings.user_args.keys(),
)
if reserved:
self.log.warning(
f"Ignoring OCR user arguments managed by paperless: "
f"{', '.join(reserved)}",
)
ocrmypdf_args = {
**ocrmypdf_args,
**{
k: v
for k, v in self.settings.user_args.items()
if k not in OCR_RESERVED_USER_ARGS
},
}
except Exception as e:
self.log.warning(
f"There is an issue with PAPERLESS_OCR_USER_ARGS, so "
+8
View File
@@ -19,6 +19,7 @@ from PIL import Image
from rest_framework import serializers
from rest_framework.authtoken.serializers import AuthTokenSerializer
from paperless.config import OCR_RESERVED_USER_ARGS
from paperless.models import ApplicationConfiguration
from paperless.network import validate_outbound_http_url
from paperless.validators import reject_dangerous_svg
@@ -251,6 +252,13 @@ class ApplicationConfigurationSerializer(
def validate_user_args(self, value):
self._require_json_object("user_args", value)
if value is not None:
reserved = sorted(OCR_RESERVED_USER_ARGS & value.keys())
if reserved:
raise serializers.ValidationError(
f"user_args may not set arguments managed by paperless: "
f"{', '.join(reserved)}.",
)
return value
def validate_barcode_tag_mapping(self, value):
@@ -243,6 +243,34 @@ class TestParserSettingsFromDb(DirectoriesMixin, FileSystemAssertsMixin, TestCas
"UseDeviceIndependentColor",
)
def test_ocr_user_args_cannot_override_reserved(self) -> None:
"""
GIVEN:
- User arguments which attempt to override arguments the parser
manages itself, alongside a legitimate argument
WHEN:
- OCR parameters are constructed
THEN:
- Reserved arguments keep the parser supplied values
- The legitimate argument is still applied
"""
instance = ApplicationConfiguration.objects.all().first()
assert instance is not None
instance.user_args = {
"sidecar": "/tmp/evil.py",
"output_file": "/tmp/out.pdf",
"input_file_or_options": "/etc/passwd",
"unpaper_args": "--pre-rotate 90",
}
instance.save()
params = self.get_params()
self.assertEqual(params["sidecar"], "sidecar.txt")
self.assertEqual(params["output_file"], "output.pdf")
self.assertEqual(params["input_file_or_options"], "input.pdf")
self.assertEqual(params["unpaper_args"], "--pre-rotate 90")
def test_ocr_user_args(self) -> None:
"""
GIVEN: