Skip to content

Commit 203bbb4

Browse files
committed
Fix REST signer to use catalog auth manager
1 parent c542e99 commit 203bbb4

File tree

4 files changed

+79
-14
lines changed

4 files changed

+79
-14
lines changed

pyiceberg/catalog/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
_ENV_CONFIG = Config()
7878

7979
TOKEN = "token"
80+
AUTH_MANAGER = "auth.manager"
8081
TYPE = "type"
8182
PY_CATALOG_IMPL = "py-catalog-impl"
8283
ICEBERG = "iceberg"

pyiceberg/catalog/rest/__init__.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,7 @@
2626
from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt
2727

2828
from pyiceberg import __version__
29-
from pyiceberg.catalog import (
30-
BOTOCORE_SESSION,
31-
TOKEN,
32-
URI,
33-
WAREHOUSE_LOCATION,
34-
Catalog,
35-
PropertiesUpdateSummary,
36-
)
29+
from pyiceberg.catalog import AUTH_MANAGER, BOTOCORE_SESSION, TOKEN, URI, WAREHOUSE_LOCATION, Catalog, PropertiesUpdateSummary
3730
from pyiceberg.catalog.rest.auth import AuthManager, AuthManagerAdapter, AuthManagerFactory, LegacyOAuth2AuthManager
3831
from pyiceberg.catalog.rest.response import _handle_non_200_response
3932
from pyiceberg.exceptions import (
@@ -49,7 +42,7 @@
4942
TableAlreadyExistsError,
5043
UnauthorizedError,
5144
)
52-
from pyiceberg.io import AWS_ACCESS_KEY_ID, AWS_REGION, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
45+
from pyiceberg.io import AWS_ACCESS_KEY_ID, AWS_REGION, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, FileIO, load_file_io
5346
from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC, PartitionSpec, assign_fresh_partition_spec_ids
5447
from pyiceberg.schema import Schema, assign_fresh_schema_ids
5548
from pyiceberg.table import (
@@ -214,6 +207,7 @@ class ListViewsResponse(IcebergBaseModel):
214207
class RestCatalog(Catalog):
215208
uri: str
216209
_session: Session
210+
_auth_manager: AuthManager | None
217211

218212
def __init__(self, name: str, **properties: str):
219213
"""Rest Catalog.
@@ -225,6 +219,7 @@ def __init__(self, name: str, **properties: str):
225219
properties: Properties that are passed along to the configuration.
226220
"""
227221
super().__init__(name, **properties)
222+
self._auth_manager: AuthManager | None = None
228223
self.uri = properties[URI]
229224
self._fetch_config()
230225
self._session = self._create_session()
@@ -259,16 +254,24 @@ def _create_session(self) -> Session:
259254
if auth_type != CUSTOM and auth_impl:
260255
raise ValueError("auth.impl can only be specified when using custom auth.type")
261256

262-
session.auth = AuthManagerAdapter(AuthManagerFactory.create(auth_impl or auth_type, auth_type_config))
257+
self._auth_manager = AuthManagerFactory.create(auth_impl or auth_type, auth_type_config)
258+
session.auth = AuthManagerAdapter(self._auth_manager)
263259
else:
264-
session.auth = AuthManagerAdapter(self._create_legacy_oauth2_auth_manager(session))
260+
self._auth_manager = self._create_legacy_oauth2_auth_manager(session)
261+
session.auth = AuthManagerAdapter(self._auth_manager)
265262

266263
# Configure SigV4 Request Signing
267264
if property_as_bool(self.properties, SIGV4, False):
268265
self._init_sigv4(session)
269266

270267
return session
271268

269+
def _load_file_io(self, properties: Properties = EMPTY_DICT, location: str | None = None) -> FileIO:
270+
merged_properties = {**self.properties, **properties}
271+
if self._auth_manager:
272+
merged_properties[AUTH_MANAGER] = self._auth_manager
273+
return load_file_io(merged_properties, location)
274+
272275
def _create_legacy_oauth2_auth_manager(self, session: Session) -> AuthManager:
273276
"""Create the LegacyOAuth2AuthManager by fetching required properties.
274277

pyiceberg/io/fsspec.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from fsspec.implementations.local import LocalFileSystem
3737
from requests import HTTPError
3838

39-
from pyiceberg.catalog import TOKEN, URI
39+
from pyiceberg.catalog import AUTH_MANAGER, TOKEN, URI
4040
from pyiceberg.exceptions import SignError
4141
from pyiceberg.io import (
4242
ADLS_ACCOUNT_HOST,
@@ -121,9 +121,19 @@ def __call__(self, request: "AWSRequest", **_: Any) -> None:
121121
signer_url = self.properties.get(S3_SIGNER_URI, self.properties[URI]).rstrip("/") # type: ignore
122122
signer_endpoint = self.properties.get(S3_SIGNER_ENDPOINT, S3_SIGNER_ENDPOINT_DEFAULT)
123123

124-
signer_headers = {}
124+
signer_headers: dict[str, str] = {}
125+
126+
auth_header: str | None = None
125127
if token := self.properties.get(TOKEN):
126-
signer_headers = {"Authorization": f"Bearer {token}"}
128+
auth_header = f"Bearer {token}"
129+
elif auth_manager := self.properties.get(AUTH_MANAGER):
130+
header = getattr(auth_manager, "auth_header", None)
131+
if callable(header):
132+
auth_header = header()
133+
134+
if auth_header:
135+
signer_headers["Authorization"] = auth_header
136+
127137
signer_headers.update(get_header_properties(self.properties))
128138

129139
signer_body = {

tests/io/test_fsspec.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from fsspec.spec import AbstractFileSystem
2929
from requests_mock import Mocker
3030

31+
from pyiceberg.catalog import AUTH_MANAGER
3132
from pyiceberg.exceptions import SignError
3233
from pyiceberg.io import fsspec
3334
from pyiceberg.io.fsspec import FsspecFileIO, S3V4RestSigner
@@ -948,3 +949,53 @@ def test_s3v4_rest_signer_forbidden(requests_mock: Mocker) -> None:
948949
"""Failed to sign request 401: {'method': 'HEAD', 'region': 'us-west-2', 'uri': 'https://bucket/metadata/snap-8048355899640248710-1-a5c8ea2d-aa1f-48e8-89f4-1fa69db8c742.avro', 'headers': {'User-Agent': ['Botocore/1.27.59 Python/3.10.7 Darwin/21.5.0']}}"""
949950
in str(exc_info.value)
950951
)
952+
953+
954+
def test_s3v4_rest_signer_uses_auth_manager(requests_mock: Mocker) -> None:
955+
new_uri = "https://bucket/metadata/snap-signed.avro"
956+
requests_mock.post(
957+
f"{TEST_URI}/v1/aws/s3/sign",
958+
json={
959+
"uri": new_uri,
960+
"headers": {
961+
"Authorization": ["AWS4-HMAC-SHA256 Credential=ASIA.../s3/aws4_request, SignedHeaders=host, Signature=abc"],
962+
"Host": ["bucket.s3.us-west-2.amazonaws.com"],
963+
},
964+
"extensions": {},
965+
},
966+
status_code=200,
967+
)
968+
969+
request = AWSRequest(
970+
method="HEAD",
971+
url="https://bucket/metadata/snap-foo.avro",
972+
headers={"User-Agent": "Botocore/1.27.59 Python/3.10.7 Darwin/21.5.0"},
973+
data=b"",
974+
params={},
975+
auth_path="/metadata/snap-foo.avro",
976+
)
977+
request.context = {
978+
"client_region": "us-west-2",
979+
"has_streaming_input": False,
980+
"auth_type": None,
981+
"signing": {"bucket": "bucket"},
982+
"retries": {"attempt": 1, "invocation-id": "75d143fb-0219-439b-872c-18213d1c8d54"},
983+
}
984+
985+
class DummyAuthManager:
986+
def __init__(self) -> None:
987+
self.calls = 0
988+
989+
def auth_header(self) -> str:
990+
self.calls += 1
991+
return "Bearer via-manager"
992+
993+
auth_manager = DummyAuthManager()
994+
995+
signer = S3V4RestSigner(properties={AUTH_MANAGER: auth_manager, "uri": TEST_URI})
996+
signer(request)
997+
998+
assert auth_manager.calls == 1
999+
assert requests_mock.last_request is not None
1000+
assert requests_mock.last_request.headers["Authorization"] == "Bearer via-manager"
1001+
assert request.url == new_uri

0 commit comments

Comments
 (0)