Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion validator/src/gm_validator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,12 @@ def _build_s3_client(config: ValidatorConfig) -> Any:

When ``config.s3_anonymous`` is set the client also signs no requests
(``botocore.UNSIGNED``) — required for OVH public-read buckets or any
bucket reachable without IAM credentials.
bucket reachable without IAM credentials — and switches to
``virtual``-hosted addressing (``bucket.endpoint/key``). OVH serves
public-read objects only via the virtual-hosted host; an *unsigned*
path-style request (``endpoint/bucket/key``) is rejected with HTTP 400
regardless of the object ACL. Signed (keyed) reads work either way, so
only the anonymous path needs the switch.

boto3-stubs types ``boto3.client()`` as an overload set keyed on the
Literal service name; passing the remaining args via ``**kwargs``
Expand All @@ -157,6 +162,7 @@ def _build_s3_client(config: ValidatorConfig) -> Any:
request_checksum_calculation="when_required",
response_checksum_validation="when_required",
signature_version=botocore.UNSIGNED if config.s3_anonymous else None,
s3={"addressing_style": "virtual"} if config.s3_anonymous else {},
)
if config.s3_endpoint_url:
return boto3.client(
Expand Down
24 changes: 24 additions & 0 deletions validator/tests/test_s3_anonymous.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,27 @@ def test_client_pins_checksums_to_when_required(anonymous: bool, endpoint_url: s
assert captured.config is not None, "expected a 'config' kwarg on boto3.client"
assert _checksum_calculation(captured.config) == "when_required"
assert _checksum_validation(captured.config) == "when_required"


def _addressing_style(config: Config) -> object:
"""Read Config.s3['addressing_style'] (the OVH virtual-hosted switch)."""
s3 = getattr(config, "s3", None) or {}
return s3.get("addressing_style")


def test_anonymous_client_uses_virtual_addressing() -> None:
"""Anonymous mode must use virtual-hosted addressing. OVH serves
public-read objects only at ``bucket.endpoint/key``; an unsigned
path-style request (``endpoint/bucket/key``) is rejected with HTTP 400
regardless of the object ACL."""
captured = _capture_client_call(_make_config(s3_anonymous=True))
assert captured.config is not None
assert _addressing_style(captured.config) == "virtual"


def test_signed_client_keeps_default_addressing() -> None:
"""Signed mode keeps default (path) addressing — the keyed gm-bucket
reads work path-style and must not be perturbed."""
captured = _capture_client_call(_make_config(s3_anonymous=False))
assert captured.config is not None
assert _addressing_style(captured.config) is None
Loading