Disable connector transports without peer pinning

This commit is contained in:
2026-07-21 15:38:09 +02:00
parent 15ade8df75
commit d5d0df792b
8 changed files with 119 additions and 31 deletions

View File

@@ -4,8 +4,7 @@ import unittest
from datetime import UTC, datetime
from unittest.mock import patch
from govoplan_files.backend.storage.connector_browse import _smb_location, browse_connector_profile
from govoplan_files.backend.storage.connector_browse import ConnectorBrowseUnsupported
from govoplan_files.backend.storage.connector_browse import ConnectorBrowseError, _smb_location, browse_connector_profile
from govoplan_files.backend.storage.connector_imports import read_connector_file
from govoplan_files.backend.storage.connector_profiles import ConnectorProfile, connector_profiles_from_payload
from govoplan_files.backend.storage.connector_providers import connector_provider_descriptors
@@ -94,6 +93,41 @@ class ConnectorProviderTests(unittest.TestCase):
self.assertEqual("93.184.216.34", location.server)
def test_smb_dns_endpoint_fails_closed_when_private_networks_are_enabled(self) -> None:
profile = ConnectorProfile(
id="private-smb",
label="Private SMB",
provider="smb",
endpoint_url="smb://files.internal.example/share",
)
with patch.dict(
"os.environ",
{"APP_ENV": "production", "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "true"},
), patch("govoplan_core.security.outbound_http.socket.getaddrinfo") as resolver, self.assertRaisesRegex(
ConnectorBrowseError,
"cannot pin DNS resolution",
):
_smb_location(profile)
resolver.assert_not_called()
def test_smb_explicit_private_ip_is_a_pinned_target(self) -> None:
profile = ConnectorProfile(
id="private-smb",
label="Private SMB",
provider="smb",
endpoint_url="smb://10.0.0.5/share",
)
with patch.dict(
"os.environ",
{"APP_ENV": "production", "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "true"},
), patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("10.0.0.5", 445))],
):
location = _smb_location(profile)
self.assertEqual("10.0.0.5", location.server)
def test_provider_descriptors_include_s3_and_reserved_microsoft_providers(self) -> None:
descriptors = {descriptor.provider: descriptor for descriptor in connector_provider_descriptors()}
@@ -150,10 +184,27 @@ class ConnectorProviderTests(unittest.TestCase):
self.assertEqual(["archive"], [item.path for item in items])
def test_s3_browse_reports_missing_optional_dependency(self) -> None:
with patch("govoplan_files.backend.storage.connector_browse.import_module", side_effect=ImportError):
with self.assertRaisesRegex(ConnectorBrowseUnsupported, "boto3"):
browse_connector_profile(s3_profile(), path="")
def test_s3_sdk_transport_fails_closed_before_client_creation_in_private_mode(self) -> None:
with patch.dict(
"os.environ",
{"APP_ENV": "production", "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "true"},
), patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("127.0.0.1", 9000))],
), patch("govoplan_files.backend.storage.connector_browse.import_module") as importer, self.assertRaisesRegex(
ConnectorBrowseError,
"until that transport supports.*DNS/IP pinning",
):
browse_connector_profile(s3_profile(), path="")
importer.assert_not_called()
def test_s3_sdk_endpoint_discovery_fails_closed(self) -> None:
with patch("govoplan_files.backend.storage.connector_browse.import_module") as importer, self.assertRaisesRegex(
ConnectorBrowseError,
"endpoint discovery.*cannot guarantee.*DNS/IP pinning",
):
browse_connector_profile(s3_profile(endpoint_url=None), path="")
importer.assert_not_called()
def test_s3_import_downloads_object_and_preserves_remote_identity(self) -> None:
client = FakeS3Client()

View File

@@ -58,7 +58,10 @@ class ConnectorHttpClientTests(unittest.TestCase):
context = MagicMock()
context.__enter__.return_value = response
with patch("govoplan_files.backend.storage.http_client._stream_connector_request", return_value=context):
with patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("93.184.216.34", 443))],
), patch("govoplan_files.backend.storage.http_client._stream_connector_request", return_value=context):
result = request_connector_bytes("GET", "https://example.test/object", max_bytes=10)
self.assertEqual(b"abcdef", result.content)
@@ -72,6 +75,9 @@ class ConnectorHttpClientTests(unittest.TestCase):
context.__enter__.return_value = response
with patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", ("93.184.216.34", 443))],
), patch(
"govoplan_files.backend.storage.http_client._stream_connector_request",
return_value=context,
), self.assertRaisesRegex(

View File

@@ -18,6 +18,20 @@ def _backend() -> S3StorageBackend:
class S3StorageBackendTests(unittest.TestCase):
def test_sdk_transport_fails_closed_in_public_and_private_modes(self) -> None:
for allow_private, address in ((False, "93.184.216.34"), (True, "10.0.0.5")):
with self.subTest(allow_private=allow_private), patch.dict(
"os.environ",
{
"APP_ENV": "production",
"GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": str(allow_private).lower(),
},
), patch(
"govoplan_core.security.outbound_http.socket.getaddrinfo",
return_value=[(2, 1, 6, "", (address, 443))],
), self.assertRaisesRegex(StorageBackendError, "until that transport supports.*DNS/IP pinning"):
_backend().client
def test_get_bytes_rejects_declared_oversize_object_without_reading(self) -> None:
body = MagicMock()
client = MagicMock()