From e9f3106657f0e27633acbca7afd8e879d6a28df9 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 21 Jul 2026 18:57:34 +0200 Subject: [PATCH] security(docs): hide runtime provider failures from users --- src/govoplan_docs/backend/api/v1/routes.py | 4 ++- tests/test_docs_context.py | 37 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/govoplan_docs/backend/api/v1/routes.py b/src/govoplan_docs/backend/api/v1/routes.py index fa72532..1019817 100644 --- a/src/govoplan_docs/backend/api/v1/routes.py +++ b/src/govoplan_docs/backend/api/v1/routes.py @@ -113,7 +113,7 @@ def docs_context( }, "evidence": { "optional_modules": optional_modules, - "sources": _evidence_sources(), + "sources": _evidence_sources() if documentation_type == "admin" else [], "documentation": documentation_layers["evidence"], }, }, @@ -356,6 +356,8 @@ def _collect_documentation_topics( try: provided_topics = tuple(provider(context)) except Exception as exc: + if documentation_type != "admin": + continue provided_topics = ( DocumentationTopic( id=f"{manifest.id}.runtime-documentation-unavailable", diff --git a/tests/test_docs_context.py b/tests/test_docs_context.py index 18c2a83..0823550 100644 --- a/tests/test_docs_context.py +++ b/tests/test_docs_context.py @@ -151,6 +151,42 @@ class DocsContextTests(unittest.TestCase): "values": ["*.example.invalid"], }) + def test_runtime_provider_failure_is_diagnostic_only(self) -> None: + def failing_provider(_context): + raise RuntimeError("sensitive provider detail") + + registry = PlatformRegistry() + registry.register(ModuleManifest( + id="private-module", + name="Private module", + version="1.0.0", + documentation_providers=(failing_provider,), + )) + principal = FakePrincipal({"docs:documentation:read"}) + + user_layers = _classify_documentation( + registry, + principal, + settings=None, + session=None, + documentation_type="user", + locale="en", + ) + admin_layers = _classify_documentation( + registry, + principal, + settings=None, + session=None, + documentation_type="admin", + locale="en", + ) + + self.assertFalse(any(user_layers.values())) + self.assertEqual( + [topic["id"] for topic in admin_layers["evidence"]], + ["private-module.runtime-documentation-unavailable"], + ) + def test_docs_default_to_user_and_admin_projection_requires_admin_authority(self) -> None: query_default = signature(docs_context).parameters["documentation_type"].default self.assertEqual(query_default.default, "user") @@ -184,6 +220,7 @@ class DocsContextTests(unittest.TestCase): self.assertNotIn("scope_count", payload["actor"]) self.assertEqual(payload["layers"]["configured"]["permissions"], []) self.assertEqual(payload["layers"]["available"]["routes"], []) + self.assertEqual(payload["layers"]["evidence"]["sources"], []) def test_topic_anchor_is_stable(self) -> None: self.assertEqual(