Release Core v0.1.32 with bounded HTTP request bodies
Module Package Release / publish-packages (push) Successful in 14s
Module Package Release / publish-packages (push) Successful in 14s
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-core"
|
||||
version = "0.1.31"
|
||||
version = "0.1.32"
|
||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -12,6 +12,9 @@ from govoplan_core.security.outbound_http import (
|
||||
)
|
||||
|
||||
|
||||
MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES = 1_000_000
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HttpFetchResponse:
|
||||
status: int
|
||||
@@ -46,11 +49,17 @@ def fetch_http(
|
||||
label: str = "URL",
|
||||
method: str = "GET",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
body: bytes | None = None,
|
||||
max_bytes: int | None = None,
|
||||
) -> HttpFetchResponse:
|
||||
if body is not None and len(body) > MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES:
|
||||
raise ValueError(
|
||||
"Outbound HTTP request body exceeds the 1000000-byte safety limit."
|
||||
)
|
||||
validated_url = validate_outbound_http_url(url, label=label)
|
||||
request = urllib.request.Request( # noqa: S310 - URL is restricted to validated HTTP(S).
|
||||
validated_url,
|
||||
data=body,
|
||||
headers=dict(headers or {}),
|
||||
method=method,
|
||||
)
|
||||
@@ -76,10 +85,19 @@ def fetch_http_text(
|
||||
label: str = "URL",
|
||||
method: str = "GET",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
body: bytes | None = None,
|
||||
encoding: str = "utf-8",
|
||||
max_bytes: int | None = None,
|
||||
) -> str:
|
||||
return fetch_http(url, timeout=timeout, label=label, method=method, headers=headers, max_bytes=max_bytes).text(encoding)
|
||||
return fetch_http(
|
||||
url,
|
||||
timeout=timeout,
|
||||
label=label,
|
||||
method=method,
|
||||
headers=headers,
|
||||
body=body,
|
||||
max_bytes=max_bytes,
|
||||
).text(encoding)
|
||||
|
||||
|
||||
class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||
|
||||
@@ -2,9 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import io
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from govoplan_core.security.http_fetch import _PolicyRedirectHandler, is_http_url, validate_http_url
|
||||
from govoplan_core.security.http_fetch import (
|
||||
_PolicyRedirectHandler,
|
||||
fetch_http,
|
||||
is_http_url,
|
||||
validate_http_url,
|
||||
)
|
||||
from govoplan_core.security.outbound_http import (
|
||||
DEFAULT_FILE_TRANSFER_BYTES,
|
||||
DEFAULT_STRUCTURED_RESPONSE_BYTES,
|
||||
@@ -21,6 +26,51 @@ from govoplan_core.security.outbound_http import (
|
||||
|
||||
|
||||
class HttpFetchTests(unittest.TestCase):
|
||||
def test_fetch_http_forwards_a_bounded_request_body(self) -> None:
|
||||
class Response(io.BytesIO):
|
||||
status = 200
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
opener = Mock()
|
||||
opener.open.return_value = Response(b"{}")
|
||||
with patch(
|
||||
"govoplan_core.security.http_fetch.validate_outbound_http_url",
|
||||
return_value="https://wiki.example.test/api.php",
|
||||
), patch(
|
||||
"govoplan_core.security.http_fetch.build_outbound_http_opener",
|
||||
return_value=opener,
|
||||
):
|
||||
response = fetch_http(
|
||||
"https://wiki.example.test/api.php",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
body=b"action=edit",
|
||||
max_bytes=1024,
|
||||
)
|
||||
|
||||
request = opener.open.call_args.args[0]
|
||||
self.assertEqual("POST", request.get_method())
|
||||
self.assertEqual(b"action=edit", request.data)
|
||||
self.assertEqual(b"{}", response.body)
|
||||
|
||||
def test_fetch_http_rejects_an_oversized_request_body_before_transport(self) -> None:
|
||||
with patch(
|
||||
"govoplan_core.security.http_fetch.validate_outbound_http_url"
|
||||
) as validate:
|
||||
with self.assertRaisesRegex(ValueError, "request body exceeds"):
|
||||
fetch_http(
|
||||
"https://wiki.example.test/api.php",
|
||||
method="POST",
|
||||
body=b"x" * 1_000_001,
|
||||
)
|
||||
validate.assert_not_called()
|
||||
|
||||
def test_validate_http_url_accepts_absolute_http_urls_without_credentials(self) -> None:
|
||||
self.assertEqual("https://example.test/catalog.json", validate_http_url("https://example.test/catalog.json"))
|
||||
self.assertTrue(is_http_url("http://example.test/catalog.json"))
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.31",
|
||||
"version": "0.1.32",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.31",
|
||||
"version": "0.1.32",
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"../../govoplan-connectors/webui": {
|
||||
"name": "@govoplan/connectors-webui",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"react": ">=19.2.7 <20",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.31",
|
||||
"version": "0.1.32",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.31",
|
||||
"version": "0.1.32",
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
|
||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.31",
|
||||
"version": "0.1.32",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.31",
|
||||
"version": "0.1.32",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user