This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git
The following commit(s) were added to refs/heads/main by this push:
new 042165ace Send the DeleteObjects body to the S3 REST signer (#4039)
042165ace is described below
commit 042165ace7e96b7d0da61810d1540c01ffff5157
Author: Sreesh Maheshwar <[email protected]>
AuthorDate: Mon Oct 26 02:35:12 2026 +0000
Send the DeleteObjects body to the S3 REST signer (#4039)
s3fs deletes files with DeleteObjects, which lists the object keys in the
request body. S3V4RestSigner never forwarded the body, so a signing
service could not authorize the keys before signing.
Also add an S3 test that writes, reads, and deletes through the REST
signer, which previously had only mocked unit tests.
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
pyiceberg/io/fsspec.py | 5 ++++-
tests/io/test_fsspec.py | 41 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/pyiceberg/io/fsspec.py b/pyiceberg/io/fsspec.py
index 98275f25a..49f10b131 100644
--- a/pyiceberg/io/fsspec.py
+++ b/pyiceberg/io/fsspec.py
@@ -125,7 +125,7 @@ class S3V4RestSigner(S3RequestSigner):
super().__init__(properties)
self._session = requests.Session()
- def __call__(self, request: "AWSRequest", **_: Any) -> None:
+ def __call__(self, request: "AWSRequest", operation_name: str | None =
None, **_: Any) -> None:
signer_url = self.properties.get(S3_SIGNER_URI,
self.properties[URI]).rstrip("/") # type: ignore
signer_endpoint = self.properties.get(S3_SIGNER_ENDPOINT,
S3_SIGNER_ENDPOINT_DEFAULT)
@@ -148,6 +148,9 @@ class S3V4RestSigner(S3RequestSigner):
"uri": request.url,
"headers": {key: [val] for key, val in request.headers.items()},
}
+ # The signing service needs the DeleteObjects body to authorize the
keys being deleted
+ if operation_name == "DeleteObjects":
+ signer_body["body"] = request.body.decode("utf-8")
response =
self._session.post(f"{signer_url}/{signer_endpoint.strip()}",
headers=signer_headers, json=signer_body)
try:
diff --git a/tests/io/test_fsspec.py b/tests/io/test_fsspec.py
index 7b9d87155..66bb319d4 100644
--- a/tests/io/test_fsspec.py
+++ b/tests/io/test_fsspec.py
@@ -20,10 +20,14 @@ import pickle
import tempfile
import threading
import uuid
+from typing import Any
from unittest import mock
import pytest
+from botocore.auth import S3SigV4Auth
from botocore.awsrequest import AWSRequest
+from botocore.config import Config
+from botocore.credentials import Credentials
from fsspec.implementations.local import LocalFileSystem
from fsspec.spec import AbstractFileSystem
from requests_mock import Mocker
@@ -1026,6 +1030,43 @@ def test_s3v4_rest_signer(requests_mock: Mocker) -> None:
}
[email protected]
+def test_s3v4_rest_signer_write_read_delete(request: pytest.FixtureRequest,
requests_mock: Mocker) -> None:
+ """Test writing, reading, and deleting an S3 file through a REST signing
service"""
+ credentials = Credentials(request.config.getoption("--s3.access-key-id"),
request.config.getoption("--s3.secret-access-key"))
+
+ def sign(signer_request: Any, _: Any) -> dict[str, Any]:
+ signer_body = signer_request.json()
+ aws_request = AWSRequest(
+ method=signer_body["method"],
+ url=signer_body["uri"],
+ headers={key: values[0] for key, values in
signer_body["headers"].items()},
+ )
+ # Upload bodies are not forwarded for signing, so leave the payload
unsigned
+ aws_request.context["client_config"] =
Config(s3={"payload_signing_enabled": False})
+ S3SigV4Auth(credentials, "s3",
signer_body["region"]).add_auth(aws_request)
+ signed_headers = ("Authorization", "X-Amz-Content-SHA256",
"X-Amz-Date")
+ return {"uri": signer_body["uri"], "headers": {key:
[aws_request.headers[key]] for key in signed_headers}}
+
+ requests_mock.post(f"{TEST_URI}/v1/aws/s3/sign", json=sign)
+ fileio = FsspecFileIO(
+ properties={"s3.endpoint": request.config.getoption("--s3.endpoint"),
"s3.signer": "S3V4RestSigner", "uri": TEST_URI}
+ )
+ filename = str(uuid.uuid4())
+ location = f"s3://warehouse/{filename}"
+
+ with fileio.new_output(location).create() as f:
+ f.write(b"foo")
+ assert fileio.new_input(location).open().read() == b"foo"
+ fileio.delete(location)
+ assert not fileio.new_input(location).exists()
+
+ signer_bodies = [signer_request.json() for signer_request in
requests_mock.request_history]
+ request_bodies = [signer_body["body"] for signer_body in signer_bodies if
"body" in signer_body]
+ assert len(request_bodies) == 1
+ assert f"<Key>{filename}</Key>" in request_bodies[0]
+
+
def test_s3v4_rest_signer_endpoint(requests_mock: Mocker) -> None:
new_uri =
"https://other-bucket/metadata/snap-8048355899640248710-1-a5c8ea2d-aa1f-48e8-89f4-1fa69db8c742.avro"
endpoint = "v1/main/s3-sign/foo.bar?e=e&b=b&k=k=k&s=s&w=w"