Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion dev/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ RUN curl --retry 5 -s https://repository.apache.org/content/groups/snapshots/org


# Download AWS bundle
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 \
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 \
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The older version of the iceberg-aws-bundle looks like it was removed. Updating the pin to fix issues with the integration tests: https://repository.apache.org/content/groups/snapshots/org/apache/iceberg/iceberg-aws-bundle/1.9.0-SNAPSHOT/

-Lo /opt/spark/jars/iceberg-aws-bundle-${ICEBERG_VERSION}.jar

COPY spark-defaults.conf /opt/spark/conf
Expand Down
File renamed without changes.
81 changes: 81 additions & 0 deletions pyiceberg/catalog/rest/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from abc import ABC, abstractmethod
from typing import Optional
from requests import PreparedRequest
from requests.auth import AuthBase

import base64

class AuthManager(ABC):
"""
Abstract base class for Authentication Managers used to supply authorization headers
to HTTP clients (e.g. requests.Session).

Subclasses must implement the `auth_header` method to return an Authorization header value.
"""
@abstractmethod
def auth_header(self) -> Optional[str]:
"""Return the Authorization header value, or None if not applicable."""
pass


class NoopAuthManager(AuthManager):
def auth_header(self) -> Optional[str]:
return None


class BasicAuthManager(AuthManager):
def __init__(self, username: str, password: str):
credentials = f"{username}:{password}"
self._token = base64.b64encode(credentials.encode()).decode()

def auth_header(self) -> str:
return f"Basic {self._token}"


class AuthManagerAdapter(AuthBase):
"""
A `requests.auth.AuthBase` adapter that integrates an `AuthManager`
into a `requests.Session` to automatically attach the appropriate
Authorization header to every request.

This adapter is useful when working with `requests.Session.auth`
and allows reuse of authentication strategies defined by `AuthManager`.
"""
def __init__(self, auth_manager: AuthManager):
"""
Args:
auth_manager (AuthManager): An instance of an AuthManager subclass.
"""
self.auth_manager = auth_manager

def __call__(self, r: PreparedRequest) -> PreparedRequest:
Comment thread
sungwy marked this conversation as resolved.
Outdated
"""
Modifies the outgoing request to include the Authorization header.

Args:
r (requests.PreparedRequest): The HTTP request being prepared.

Returns:
requests.PreparedRequest: The modified request with Authorization header.
"""
auth_header = self.auth_manager.auth_header()
if auth_header:
Comment thread
sungwy marked this conversation as resolved.
Outdated
r.headers['Authorization'] = auth_header
return r
63 changes: 63 additions & 0 deletions tests/catalog/test_rest_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from pyiceberg.catalog.rest.auth import AuthManagerAdapter, NoopAuthManager, BasicAuthManager

import base64
import pytest
import requests
from requests_mock import Mocker

TEST_URI = "https://iceberg-test-catalog/"

@pytest.fixture
def rest_mock(requests_mock: Mocker) -> Mocker:
requests_mock.get(
TEST_URI,
json={},
status_code=200,
)
return requests_mock


def test_noop_auth_header(rest_mock: Mocker):
auth_manager = NoopAuthManager()
session = requests.Session()
session.auth = AuthManagerAdapter(auth_manager)

response = session.get(TEST_URI)
history = rest_mock.request_history
assert len(history) == 1
actual_headers = history[0].headers
assert "Authorization" not in actual_headers


def test_basic_auth_header(rest_mock: Mocker):
username = "testuser"
password = "testpassword"
expected_token = base64.b64encode(f"{username}:{password}".encode()).decode()
expected_header = f"Basic {expected_token}"

auth_manager = BasicAuthManager(username=username, password=password)
session = requests.Session()
session.auth = AuthManagerAdapter(auth_manager)

response = session.get(TEST_URI)
history = rest_mock.request_history
assert len(history) == 1
actual_headers = history[0].headers
assert actual_headers["Authorization"] == expected_header
Loading