Compare commits

...
2 Commits
Author SHA1 Message Date
zemion 6ccef162f6 Release Core v0.1.32 with bounded HTTP request bodies
Module Package Release / publish-packages (push) Successful in 14s
2026-08-22 14:32:24 +02:00
zemion 48dac139a5 feat(wiki): compose governed Wiki WebUI
Module Package Release / publish-packages (push) Successful in 12s
2026-08-22 13:32:20 +02:00
9 changed files with 125 additions and 12 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-core"
version = "0.1.30"
version = "0.1.32"
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
readme = "README.md"
requires-python = ">=3.12"
+19 -1
View File
@@ -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):
+52 -2
View File
@@ -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"))
+24 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.30",
"version": "0.1.32",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@govoplan/core-webui",
"version": "0.1.30",
"version": "0.1.32",
"dependencies": {
"@govoplan/access-webui": "file:../../govoplan-access/webui",
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
@@ -52,6 +52,7 @@
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
"@govoplan/views-webui": "file:../../govoplan-views/webui",
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
"@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2",
@@ -242,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",
@@ -822,6 +823,22 @@
}
}
},
"../../govoplan-wiki/webui": {
"name": "@govoplan/wiki-webui",
"version": "0.1.20",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.31",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
}
},
"../../govoplan-workflow/webui": {
"name": "@govoplan/workflow-webui",
"version": "0.1.21",
@@ -1768,6 +1785,10 @@
"resolved": "../../govoplan-voting/webui",
"link": true
},
"node_modules/@govoplan/wiki-webui": {
"resolved": "../../govoplan-wiki/webui",
"link": true
},
"node_modules/@govoplan/workflow-webui": {
"resolved": "../../govoplan-workflow/webui",
"link": true
+19 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.30",
"version": "0.1.32",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@govoplan/core-webui",
"version": "0.1.30",
"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",
@@ -24,6 +24,7 @@
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18",
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20",
"@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2",
"@tiptap/pm": "^3.29.2",
@@ -1032,6 +1033,22 @@
}
}
},
"node_modules/@govoplan/wiki-webui": {
"version": "0.1.20",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#66c91351c9eb693ace606c9b69dd5cd804d7531b",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.31",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.30",
"version": "0.1.32",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -102,6 +102,7 @@
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
"@govoplan/views-webui": "file:../../govoplan-views/webui",
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
"@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.30",
"version": "0.1.32",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -42,6 +42,7 @@
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20",
"@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2",
"@tiptap/pm": "^3.29.2",
+4 -1
View File
@@ -46,6 +46,7 @@ const packageByModule = {
tickets: "@govoplan/tickets-webui",
views: "@govoplan/views-webui",
voting: "@govoplan/voting-webui",
wiki: "@govoplan/wiki-webui",
workflow: "@govoplan/workflow-webui"
};
@@ -79,6 +80,8 @@ const cases = [
{ name: "forms-runtime", modules: ["forms", "forms_runtime"] },
{ name: "tickets-only", modules: ["tickets"] },
{ name: "tickets-with-helpdesk-and-cases", modules: ["tickets", "helpdesk", "cases"] },
{ name: "wiki-only", modules: ["wiki"] },
{ name: "wiki-with-files-search", modules: ["wiki", "files", "search"] },
{ name: "mail-only", modules: ["mail"] },
{ name: "notifications-only", modules: ["notifications"] },
{ name: "organizations-only", modules: ["organizations"] },
@@ -110,7 +113,7 @@ const cases = [
{ name: "tasks-only", modules: ["access", "tasks"] },
{ name: "tasks-with-contributors", modules: ["access", "approvals", "postbox", "workflow", "dashboard", "tasks"] },
{ name: "voting-only", modules: ["access", "voting"] },
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "helpdesk", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "tickets", "voting"] }
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "helpdesk", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "tickets", "voting", "wiki"] }
];
const npmExec = process.env.npm_execpath;
+2
View File
@@ -56,6 +56,7 @@ const defaultWebModulePackages = [
"@govoplan/tickets-webui",
"@govoplan/views-webui",
"@govoplan/voting-webui",
"@govoplan/wiki-webui",
"@govoplan/workflow-webui"
];
@@ -289,6 +290,7 @@ export default defineConfig({
fileURLToPath(new URL('../../govoplan-tickets/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-voting/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-wiki/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
]
},