Skip to content

Commit f36d242

Browse files
committed
introduce auth manager
1 parent 1588701 commit f36d242

File tree

4 files changed

+145
-1
lines changed

4 files changed

+145
-1
lines changed

dev/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ RUN curl --retry 5 -s https://repository.apache.org/content/groups/snapshots/org
5252

5353

5454
# Download AWS bundle
55-
RUN curl --retry 5 -s https://repository.apache.org/content/groups/snapshots/org/apache/iceberg/iceberg-aws-bundle/1.9.0-SNAPSHOT/iceberg-aws-bundle-1.9.0-20250408.002722-86.jar \
55+
RUN curl --retry 5 -s https://repository.apache.org/content/groups/snapshots/org/apache/iceberg/iceberg-aws-bundle/1.9.0-SNAPSHOT/iceberg-aws-bundle-1.9.0-20250409.002731-88.jar \
5656
-Lo /opt/spark/jars/iceberg-aws-bundle-${ICEBERG_VERSION}.jar
5757

5858
COPY spark-defaults.conf /opt/spark/conf

pyiceberg/catalog/rest/auth.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from abc import ABC, abstractmethod
19+
from typing import Optional
20+
from requests import PreparedRequest
21+
from requests.auth import AuthBase
22+
23+
import base64
24+
25+
class AuthManager(ABC):
26+
"""
27+
Abstract base class for Authentication Managers used to supply authorization headers
28+
to HTTP clients (e.g. requests.Session).
29+
30+
Subclasses must implement the `auth_header` method to return an Authorization header value.
31+
"""
32+
@abstractmethod
33+
def auth_header(self) -> Optional[str]:
34+
"""Return the Authorization header value, or None if not applicable."""
35+
pass
36+
37+
38+
class NoopAuthManager(AuthManager):
39+
def auth_header(self) -> Optional[str]:
40+
return None
41+
42+
43+
class BasicAuthManager(AuthManager):
44+
def __init__(self, username: str, password: str):
45+
credentials = f"{username}:{password}"
46+
self._token = base64.b64encode(credentials.encode()).decode()
47+
48+
def auth_header(self) -> str:
49+
return f"Basic {self._token}"
50+
51+
52+
class AuthManagerAdapter(AuthBase):
53+
"""
54+
A `requests.auth.AuthBase` adapter that integrates an `AuthManager`
55+
into a `requests.Session` to automatically attach the appropriate
56+
Authorization header to every request.
57+
58+
This adapter is useful when working with `requests.Session.auth`
59+
and allows reuse of authentication strategies defined by `AuthManager`.
60+
"""
61+
def __init__(self, auth_manager: AuthManager):
62+
"""
63+
Args:
64+
auth_manager (AuthManager): An instance of an AuthManager subclass.
65+
"""
66+
self.auth_manager = auth_manager
67+
68+
def __call__(self, r: PreparedRequest) -> PreparedRequest:
69+
"""
70+
Modifies the outgoing request to include the Authorization header.
71+
72+
Args:
73+
r (requests.PreparedRequest): The HTTP request being prepared.
74+
75+
Returns:
76+
requests.PreparedRequest: The modified request with Authorization header.
77+
"""
78+
auth_header = self.auth_manager.auth_header()
79+
if auth_header:
80+
r.headers['Authorization'] = auth_header
81+
return r

tests/catalog/test_rest_auth.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from pyiceberg.catalog.rest.auth import AuthManagerAdapter, NoopAuthManager, BasicAuthManager
19+
20+
import base64
21+
import pytest
22+
import requests
23+
from requests_mock import Mocker
24+
25+
TEST_URI = "https://iceberg-test-catalog/"
26+
27+
@pytest.fixture
28+
def rest_mock(requests_mock: Mocker) -> Mocker:
29+
requests_mock.get(
30+
TEST_URI,
31+
json={},
32+
status_code=200,
33+
)
34+
return requests_mock
35+
36+
37+
def test_noop_auth_header(rest_mock: Mocker):
38+
auth_manager = NoopAuthManager()
39+
session = requests.Session()
40+
session.auth = AuthManagerAdapter(auth_manager)
41+
42+
response = session.get(TEST_URI)
43+
history = rest_mock.request_history
44+
assert len(history) == 1
45+
actual_headers = history[0].headers
46+
assert "Authorization" not in actual_headers
47+
48+
49+
def test_basic_auth_header(rest_mock: Mocker):
50+
username = "testuser"
51+
password = "testpassword"
52+
expected_token = base64.b64encode(f"{username}:{password}".encode()).decode()
53+
expected_header = f"Basic {expected_token}"
54+
55+
auth_manager = BasicAuthManager(username=username, password=password)
56+
session = requests.Session()
57+
session.auth = AuthManagerAdapter(auth_manager)
58+
59+
response = session.get(TEST_URI)
60+
history = rest_mock.request_history
61+
assert len(history) == 1
62+
actual_headers = history[0].headers
63+
assert actual_headers["Authorization"] == expected_header

0 commit comments

Comments
 (0)