mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-02 07:57:15 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47288d4559 | ||
|
|
d78754bff1 |
@@ -50,18 +50,18 @@ repos:
|
||||
- 'prettier-plugin-organize-imports@4.3.0'
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.16.1
|
||||
rev: v0.16.4
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "v2.26.0"
|
||||
rev: "v2.28.1"
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
additional_dependencies: [tomli]
|
||||
# Dockerfile hooks
|
||||
- repo: https://github.com/AleksaC/hadolint-py
|
||||
rev: v2.14.0
|
||||
rev: v2.15.1
|
||||
hooks:
|
||||
- id: hadolint
|
||||
# Shell script hooks
|
||||
|
||||
@@ -2088,6 +2088,12 @@ password. All of these options come from their similarly-named [Django settings]
|
||||
|
||||
Defaults to "always".
|
||||
|
||||
#### [`PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=<bool>`](#PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS) {#PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS}
|
||||
|
||||
: If set to false, Paperless blocks remote OCR endpoint URLs that resolve to non-public addresses (e.g., localhost, etc).
|
||||
|
||||
Defaults to True.
|
||||
|
||||
## AI {#ai}
|
||||
|
||||
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
||||
|
||||
@@ -1063,3 +1063,79 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("non-public address", str(response.data).lower())
|
||||
|
||||
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=False)
|
||||
def test_update_remote_ocr_endpoint_blocks_internal_endpoint_when_disallowed(
|
||||
self,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Internal remote OCR endpoints are disallowed
|
||||
WHEN:
|
||||
- The config is updated with a remote OCR endpoint resolving internally
|
||||
THEN:
|
||||
- The request is rejected
|
||||
"""
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}1/",
|
||||
json.dumps(
|
||||
{
|
||||
"remote_ocr_endpoint": "http://127.0.0.1:5000",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("non-public address", str(response.data).lower())
|
||||
|
||||
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=True)
|
||||
def test_update_remote_ocr_endpoint_allows_internal_endpoint_by_default(
|
||||
self,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Internal remote OCR endpoints are allowed (the default)
|
||||
WHEN:
|
||||
- The config is updated with a remote OCR endpoint resolving internally
|
||||
THEN:
|
||||
- The request is accepted, preserving existing self-hosted deployments
|
||||
"""
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}1/",
|
||||
json.dumps(
|
||||
{
|
||||
"remote_ocr_endpoint": "http://127.0.0.1:5000",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["remote_ocr_endpoint"],
|
||||
"http://127.0.0.1:5000",
|
||||
)
|
||||
|
||||
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=False)
|
||||
def test_update_remote_ocr_endpoint_empty_value_skips_validation(
|
||||
self,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Internal remote OCR endpoints are disallowed
|
||||
WHEN:
|
||||
- The config is updated with an empty remote OCR endpoint
|
||||
THEN:
|
||||
- The request is accepted; clearing the field never needs
|
||||
outbound URL validation
|
||||
"""
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}1/",
|
||||
json.dumps(
|
||||
{
|
||||
"remote_ocr_endpoint": "",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["remote_ocr_endpoint"], "")
|
||||
|
||||
@@ -32,6 +32,8 @@ if TYPE_CHECKING:
|
||||
import datetime
|
||||
from types import TracebackType
|
||||
|
||||
from azure.core.pipeline import PipelineRequest
|
||||
|
||||
from paperless.parsers import MetadataEntry
|
||||
from paperless.parsers import ParserContext
|
||||
|
||||
@@ -436,9 +438,45 @@ class RemoteDocumentParser:
|
||||
from azure.ai.documentintelligence.models import DocumentContentFormat
|
||||
from azure.core.credentials import AzureKeyCredential
|
||||
|
||||
from paperless.network import validate_outbound_http_url
|
||||
|
||||
allow_internal = settings.REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS
|
||||
|
||||
try:
|
||||
validate_outbound_http_url(config.endpoint, allow_internal=allow_internal)
|
||||
except ValueError as e:
|
||||
raise ParseError(f"Invalid remote OCR endpoint: {e}") from e
|
||||
|
||||
def _revalidate_request_host(request: PipelineRequest) -> None:
|
||||
"""Re-validates the destination host of every request sent.
|
||||
|
||||
The check above only covers the moment the client is built. A
|
||||
single analysis involves several requests spread over the
|
||||
polling loop below, and any one of them can be redirected.
|
||||
Wiring this through ``raw_request_hook`` (Azure's built-in
|
||||
CustomHookPolicy) rather than a custom policy means it runs
|
||||
*after* RedirectPolicy in the pipeline, so it sees - and
|
||||
re-checks - every actual outbound URL, including redirect
|
||||
targets, not just the original request.
|
||||
"""
|
||||
validate_outbound_http_url(
|
||||
request.http_request.url,
|
||||
allow_internal=allow_internal,
|
||||
)
|
||||
|
||||
client = DocumentIntelligenceClient(
|
||||
endpoint=config.endpoint,
|
||||
credential=AzureKeyCredential(config.api_key),
|
||||
raw_request_hook=_revalidate_request_host,
|
||||
# AzureKeyCredential is sent as Ocp-Apim-Subscription-Key, which
|
||||
# Azure's default SensitiveHeaderCleanupPolicy does not strip on
|
||||
# a cross-domain redirect (only Authorization and
|
||||
# x-ms-authorization-auxiliary are, by default).
|
||||
blocked_redirect_headers=[
|
||||
"Authorization",
|
||||
"x-ms-authorization-auxiliary",
|
||||
"Ocp-Apim-Subscription-Key",
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -305,6 +305,22 @@ class ApplicationConfigurationSerializer(
|
||||
|
||||
validate_llm_embedding_endpoint = validate_llm_endpoint
|
||||
|
||||
def validate_remote_ocr_endpoint(self, value: str | None) -> str | None:
|
||||
if not value:
|
||||
return value
|
||||
|
||||
try:
|
||||
validate_outbound_http_url(
|
||||
value,
|
||||
allow_internal=settings.REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid remote OCR endpoint: {e.args[0]}, see logs for details",
|
||||
) from e
|
||||
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = ApplicationConfiguration
|
||||
fields = "__all__"
|
||||
|
||||
@@ -1208,6 +1208,10 @@ REMOTE_OCR_MODE = get_choice_from_env(
|
||||
{"always", "workflow_only"},
|
||||
default="always",
|
||||
)
|
||||
REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS = get_bool_from_env(
|
||||
"PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS",
|
||||
"true",
|
||||
)
|
||||
|
||||
################################################################################
|
||||
# AI Settings #
|
||||
|
||||
Reference in New Issue
Block a user