Pin S3 and SMB connector peers

This commit is contained in:
2026-08-04 10:40:46 +02:00
parent 0c68e904cf
commit 92e649477f
15 changed files with 840 additions and 112 deletions
@@ -15,8 +15,8 @@ from defusedxml import ElementTree as SafeElementTree
from govoplan_core.security.outbound_http import (
OutboundHttpError,
validate_unpinned_sdk_host,
validate_unpinned_sdk_http_url,
validate_outbound_host,
validate_outbound_http_url,
)
from govoplan_files.backend.storage.http_client import ConnectorHttpError, request_connector_bytes
@@ -27,6 +27,12 @@ from govoplan_files.backend.storage.connector_deployment import (
validate_connector_tls_metadata,
)
from govoplan_files.backend.storage.connector_profiles import ConnectorProfile
from govoplan_files.backend.storage.sdk_peer_pinning import (
SdkPeerPinningError,
create_pinned_s3_client,
install_pinned_smb_transport,
pinned_smb_connection_cache,
)
class ConnectorBrowseError(RuntimeError):
@@ -246,6 +252,28 @@ def _browse_smb(profile: ConnectorProfile, *, path: str) -> list[ConnectorBrowse
def _browse_s3(profile: ConnectorProfile, *, path: str, library_id: str | None, continuation_token: str | None) -> list[ConnectorBrowseItem]:
client = _s3_client(profile)
try:
return _browse_s3_with_client(
client,
profile=profile,
path=path,
library_id=library_id,
continuation_token=continuation_token,
)
finally:
close = getattr(client, "close", None)
if callable(close):
close()
def _browse_s3_with_client(
client: Any,
*,
profile: ConnectorProfile,
path: str,
library_id: str | None,
continuation_token: str | None,
) -> list[ConnectorBrowseItem]:
bucket = _s3_bucket(profile, library_id)
if not bucket:
try:
@@ -313,23 +341,22 @@ def _s3_client(profile: ConnectorProfile) -> Any:
raise ConnectorBrowseError("Secret-ref S3 credentials need a runtime secret resolver before live browsing")
if profile.endpoint_url:
try:
endpoint_url = validate_unpinned_sdk_http_url(
endpoint_url = validate_outbound_http_url(
profile.endpoint_url,
label="S3 connector endpoint",
)
except OutboundHttpError as exc:
raise ConnectorBrowseError(str(exc)) from exc
else:
raise ConnectorBrowseError(
"S3 connector endpoint discovery uses an SDK transport that cannot guarantee connection-time DNS/IP "
"pinning; live S3 access is disabled until that transport supports pinning"
)
endpoint_url = None
try:
boto3 = import_module("boto3")
config_module = import_module("botocore.config")
unsigned = import_module("botocore").UNSIGNED
except ImportError as exc:
raise ConnectorBrowseUnsupported("S3 connector browsing requires the optional boto3 dependency") from exc
kwargs: dict[str, object] = {"endpoint_url": endpoint_url}
kwargs: dict[str, object] = {}
if endpoint_url:
kwargs["endpoint_url"] = endpoint_url
region = _metadata_string(profile, "region") or _metadata_string(profile, "aws_region")
if region:
kwargs["region_name"] = region
@@ -346,10 +373,21 @@ def _s3_client(profile: ConnectorProfile) -> Any:
if verify is not None:
kwargs["verify"] = verify
addressing_style = _s3_addressing_style(profile)
config_values: dict[str, object] = {
"proxies": {},
"retries": {"mode": "standard", "max_attempts": 4},
}
if addressing_style:
kwargs["config"] = config_module.Config(s3={"addressing_style": addressing_style})
config_values["s3"] = {"addressing_style": addressing_style}
if bool(access_key) != bool(secret_key):
raise ConnectorBrowseError("S3 connectors require both an access key and a secret key")
if not access_key:
config_values["signature_version"] = unsigned
kwargs["config"] = config_module.Config(**config_values)
try:
return boto3.client("s3", **kwargs)
return create_pinned_s3_client(**kwargs)
except SdkPeerPinningError as exc:
raise ConnectorBrowseError(str(exc)) from exc
except Exception as exc: # pragma: no cover - concrete exception types are dependency-version specific
raise ConnectorBrowseError(f"S3 connector could not be initialized: {exc}") from exc
@@ -783,7 +821,7 @@ def _smb_location(profile: ConnectorProfile) -> _SmbLocation:
raise ConnectorBrowseError("SMB connector endpoint_url must include a server")
port = parsed.port or _int(profile.metadata.get("port")) or 445
try:
validate_unpinned_sdk_host(server, port=port, label="SMB connector endpoint")
validate_outbound_host(server, port=port, label="SMB connector endpoint")
except OutboundHttpError as exc:
raise ConnectorBrowseError(str(exc)) from exc
path_parts = [part for part in unquote(parsed.path or "").strip("/").split("/") if part]
@@ -810,6 +848,7 @@ def _smb_unc_path(location: _SmbLocation, path: str) -> str:
def _smb_client_kwargs(profile: ConnectorProfile, location: _SmbLocation) -> dict[str, object]:
kwargs: dict[str, object] = {
"port": location.port,
"connection_cache": pinned_smb_connection_cache(),
"require_signing": _metadata_bool(profile, "require_signing", default=True),
"auth_protocol": _metadata_string(profile, "auth_protocol") or "ntlm",
}
@@ -845,9 +884,11 @@ def _profile_token(profile: ConnectorProfile) -> str | None:
def _smbclient_module() -> Any:
try:
return import_module("smbclient")
return install_pinned_smb_transport(import_module("smbclient"))
except ImportError as exc:
raise ConnectorBrowseUnsupported("SMB connector browsing requires the optional smbprotocol dependency") from exc
except SdkPeerPinningError as exc:
raise ConnectorBrowseUnsupported(str(exc)) from exc
def _smb_entry_stat(entry: object) -> object | None: