diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 677eb43..1a26e0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.13.1 + rev: v0.13.3 hooks: # Run the linter. - id: ruff-check diff --git a/src/otdf_python/auth_headers.py b/src/otdf_python/auth_headers.py index da6124a..830c144 100644 --- a/src/otdf_python/auth_headers.py +++ b/src/otdf_python/auth_headers.py @@ -10,7 +10,7 @@ class AuthHeaders: """ auth_header: str - dpop_header: str + dpop_header: str = "" def get_auth_header(self) -> str: """Returns the authorization header.""" @@ -19,3 +19,15 @@ def get_auth_header(self) -> str: def get_dpop_header(self) -> str: """Returns the DPoP header.""" return self.dpop_header + + def to_dict(self) -> dict[str, str]: + """ + Convert authentication headers to a dictionary for use with HTTP clients. + + Returns: + Dictionary with 'Authorization' header and optionally 'DPoP' header + """ + headers = {"Authorization": self.auth_header} + if self.dpop_header: + headers["DPoP"] = self.dpop_header + return headers diff --git a/src/otdf_python/kas_connect_rpc_client.py b/src/otdf_python/kas_connect_rpc_client.py index 3b39021..e70fe63 100644 --- a/src/otdf_python/kas_connect_rpc_client.py +++ b/src/otdf_python/kas_connect_rpc_client.py @@ -9,6 +9,8 @@ from otdf_python_proto.kas import kas_pb2 from otdf_python_proto.kas.kas_pb2_connect import AccessServiceClient +from otdf_python.auth_headers import AuthHeaders + from .sdk_exceptions import SDKException @@ -69,7 +71,11 @@ def _prepare_auth_headers(self, access_token): Dictionary with authentication headers or None """ if access_token: - return {"Authorization": f"Bearer {access_token}"} + auth_headers = AuthHeaders( + auth_header=f"Bearer {access_token}", + dpop_header="", # Empty for now, ready for future DPoP support + ) + return auth_headers.to_dict() return None def get_public_key(self, normalized_kas_url, kas_info, access_token=None): diff --git a/src/otdf_python/sdk.py b/src/otdf_python/sdk.py index b23d9d9..8cbb43d 100644 --- a/src/otdf_python/sdk.py +++ b/src/otdf_python/sdk.py @@ -6,42 +6,12 @@ from io import BytesIO from typing import Any, BinaryIO -from otdf_python.config import NanoTDFConfig, TDFConfig +from otdf_python.config import KASInfo, NanoTDFConfig, TDFConfig from otdf_python.nanotdf import NanoTDF from otdf_python.sdk_exceptions import SDKException from otdf_python.tdf import TDF, TDFReader, TDFReaderConfig -# Stubs for service client interfaces (to be implemented) -class AttributesServiceClientInterface: ... - - -class NamespaceServiceClientInterface: ... - - -class SubjectMappingServiceClientInterface: ... - - -class ResourceMappingServiceClientInterface: ... - - -class AuthorizationServiceClientInterface: ... - - -class KeyAccessServerRegistryServiceClientInterface: ... - - -# Placeholder for ProtocolClient and Interceptor -class ProtocolClient: ... - - -class Interceptor: ... # Can be dict in Python implementation - - -# Placeholder for TrustManager -class TrustManager: ... - - class KAS(AbstractContextManager): """ KAS (Key Access Service) interface to define methods related to key access and management. @@ -71,7 +41,6 @@ def __init__( token_source=None, sdk_ssl_verify=True, use_plaintext=False, - auth_headers: dict | None = None, ): """ Initialize the KAS client @@ -81,7 +50,6 @@ def __init__( token_source: Function that returns an authentication token sdk_ssl_verify: Whether to verify SSL certificates use_plaintext: Whether to use plaintext HTTP connections instead of HTTPS - auth_headers: Dictionary of authentication headers to include in requests """ from .kas_client import KASClient @@ -94,7 +62,6 @@ def __init__( # Store the parameters for potential use self._sdk_ssl_verify = sdk_ssl_verify self._use_plaintext = use_plaintext - self._auth_headers = auth_headers def get_ec_public_key(self, kas_info: Any, curve: Any) -> Any: """ @@ -179,12 +146,14 @@ def __exit__(self, exc_type, exc_val, exc_tb): class SDK(AbstractContextManager): def new_tdf_config( - self, attributes: list[str] | None = None, **kwargs + self, + attributes: list[str] | None = None, + kas_info_list: list[KASInfo] | None = None, + **kwargs, ) -> TDFConfig: """ Create a TDFConfig with default kas_info_list from the SDK's platform_url. """ - from otdf_python.config import KASInfo if self.platform_url is None: raise SDKException("Cannot create TDFConfig: SDK platform_url is not set.") @@ -232,10 +201,8 @@ def new_tdf_config( # Use existing port with the determined scheme kas_url = f"{scheme}://{parsed_url.hostname}:{parsed_url.port}{parsed_url.path.rstrip('/')}/kas" - kas_info = KASInfo(url=kas_url, default=True) - # Accept user override for kas_info_list if provided - kas_info_list = kwargs.pop("kas_info_list", None) if kas_info_list is None: + kas_info = KASInfo(url=kas_url, default=True) kas_info_list = [kas_info] return TDFConfig( kas_info_list=kas_info_list, attributes=attributes or [], **kwargs @@ -251,30 +218,6 @@ class Services(AbstractContextManager): The Services interface provides access to various platform service clients and KAS. """ - def attributes(self) -> AttributesServiceClientInterface: - """Returns the attributes service client""" - raise NotImplementedError - - def namespaces(self) -> NamespaceServiceClientInterface: - """Returns the namespaces service client""" - raise NotImplementedError - - def subject_mappings(self) -> SubjectMappingServiceClientInterface: - """Returns the subject mappings service client""" - raise NotImplementedError - - def resource_mappings(self) -> ResourceMappingServiceClientInterface: - """Returns the resource mappings service client""" - raise NotImplementedError - - def authorization(self) -> AuthorizationServiceClientInterface: - """Returns the authorization service client""" - raise NotImplementedError - - def kas_registry(self) -> KeyAccessServerRegistryServiceClientInterface: - """Returns the KAS registry service client""" - raise NotImplementedError - def kas(self) -> KAS: """ Returns the KAS client for key access operations. @@ -292,9 +235,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): def __init__( self, services: "SDK.Services", - trust_manager: TrustManager | None = None, - auth_interceptor: Interceptor | dict[str, str] | None = None, - platform_services_client: ProtocolClient | None = None, platform_url: str | None = None, ssl_verify: bool = True, use_plaintext: bool = False, @@ -304,17 +244,11 @@ def __init__( Args: services: The services interface implementation - trust_manager: Optional trust manager for SSL validation - auth_interceptor: Optional auth interceptor for API requests - platform_services_client: Optional client for platform services platform_url: Optional platform base URL ssl_verify: Whether to verify SSL certificates (default: True) use_plaintext: Whether to use HTTP instead of HTTPS (default: False) """ self.services = services - self.trust_manager = trust_manager - self.auth_interceptor = auth_interceptor - self.platform_services_client = platform_services_client self.platform_url = platform_url self.ssl_verify = ssl_verify self._use_plaintext = use_plaintext @@ -332,18 +266,6 @@ def get_services(self) -> "SDK.Services": """Returns the services interface""" return self.services - def get_trust_manager(self) -> TrustManager | None: - """Returns the trust manager if set""" - return self.trust_manager - - def get_auth_interceptor(self) -> Interceptor | dict[str, str] | None: - """Returns the auth interceptor if set""" - return self.auth_interceptor - - def get_platform_services_client(self) -> ProtocolClient | None: - """Returns the platform services client if set""" - return self.platform_services_client - def get_platform_url(self) -> str | None: """Returns the platform URL if set""" return self.platform_url diff --git a/src/otdf_python/sdk_builder.py b/src/otdf_python/sdk_builder.py index 9f4f5e4..322452d 100644 --- a/src/otdf_python/sdk_builder.py +++ b/src/otdf_python/sdk_builder.py @@ -7,7 +7,6 @@ import ssl from dataclasses import dataclass from pathlib import Path -from typing import Any import httpx @@ -344,32 +343,6 @@ def _get_token_from_client_credentials(self) -> str: except Exception as e: raise AutoConfigureException(f"Error during token acquisition: {e!s}") - def _create_auth_interceptor(self) -> Any: - """ - Creates an authentication interceptor for API requests (httpx). - Returns: - Any: An auth interceptor object - Raises: - AutoConfigureException: If auth configuration fails - """ - # For now, this is just a placeholder returning a dict with auth headers - # In a real implementation, this would create a proper interceptor object - # that injects auth headers into httpx requests - - token = None - - if self.auth_token: - # Use provided token - token = self.auth_token - elif self.oauth_config: - # Get token from OAuth - token = self._get_token_from_client_credentials() - - if token: - return {"Authorization": f"Bearer {token}"} - - return None - def _create_services(self) -> SDK.Services: """ Creates service client instances. @@ -383,13 +356,11 @@ def _create_services(self) -> SDK.Services: # connecting to the platform endpoints ssl_verify = not self.insecure_skip_verify - auth_interceptor = self._create_auth_interceptor() class ServicesImpl(SDK.Services): def __init__(self, builder_instance): self.closed = False self._ssl_verify = ssl_verify - self._auth_headers = auth_interceptor if auth_interceptor else {} self._builder = builder_instance def kas(self) -> KAS: @@ -433,16 +404,12 @@ def build(self) -> SDK: if not self.platform_endpoint: raise AutoConfigureException("Platform endpoint is not set") - # Create the auth interceptor - auth_interceptor = self._create_auth_interceptor() - # Create services services = self._create_services() # Return the SDK instance, platform_url is set for new_tdf_config return SDK( services=services, - auth_interceptor=auth_interceptor, platform_url=self.platform_endpoint, ssl_verify=not self.insecure_skip_verify, use_plaintext=getattr(self, "use_plaintext", False), diff --git a/tests/test_inner_classes.py b/tests/test_inner_classes.py index 3b5ea89..ce794ba 100644 --- a/tests/test_inner_classes.py +++ b/tests/test_inner_classes.py @@ -13,6 +13,27 @@ def test_auth_headers(self): self.assertEqual(headers.get_auth_header(), "Bearer token123") self.assertEqual(headers.get_dpop_header(), "dpop456") + def test_auth_headers_to_dict_with_dpop(self): + headers = AuthHeaders("Bearer token123", "dpop456") + headers_dict = headers.to_dict() + self.assertEqual(headers_dict["Authorization"], "Bearer token123") + self.assertEqual(headers_dict["DPoP"], "dpop456") + self.assertEqual(len(headers_dict), 2) + + def test_auth_headers_to_dict_without_dpop(self): + headers = AuthHeaders("Bearer token123", "") + headers_dict = headers.to_dict() + self.assertEqual(headers_dict["Authorization"], "Bearer token123") + self.assertNotIn("DPoP", headers_dict) + self.assertEqual(len(headers_dict), 1) + + def test_auth_headers_default_dpop(self): + headers = AuthHeaders("Bearer token123") + self.assertEqual(headers.dpop_header, "") + headers_dict = headers.to_dict() + self.assertEqual(headers_dict["Authorization"], "Bearer token123") + self.assertNotIn("DPoP", headers_dict) + class TestKASInfo(unittest.TestCase): def test_kas_info_clone(self): diff --git a/tests/test_sdk.py b/tests/test_sdk.py index c58126e..9c217f1 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -17,9 +17,6 @@ def test_sdk_init_and_close(): services = DummyServices() sdk = SDK(services) assert sdk.get_services() is services - assert sdk.get_trust_manager() is None - assert sdk.get_auth_interceptor() is None - assert sdk.get_platform_services_client() is None assert sdk.get_platform_url() is None # Test context manager exit calls close with SDK(services): diff --git a/tests/test_sdk_builder.py b/tests/test_sdk_builder.py index 0fae7ae..756b2c7 100644 --- a/tests/test_sdk_builder.py +++ b/tests/test_sdk_builder.py @@ -211,5 +211,4 @@ def test_build_success(): # Verify the SDK was created correctly assert isinstance(sdk, SDK) assert sdk.platform_url == "https://example.com" - assert sdk.auth_interceptor == {"Authorization": "Bearer test-token"} assert sdk.get_services() is mock_services diff --git a/tests/test_sdk_mock.py b/tests/test_sdk_mock.py index 088a452..d7c574f 100644 --- a/tests/test_sdk_mock.py +++ b/tests/test_sdk_mock.py @@ -1,13 +1,4 @@ -from otdf_python.sdk import ( - KAS, - SDK, - AttributesServiceClientInterface, - AuthorizationServiceClientInterface, - KeyAccessServerRegistryServiceClientInterface, - NamespaceServiceClientInterface, - ResourceMappingServiceClientInterface, - SubjectMappingServiceClientInterface, -) +from otdf_python.sdk import KAS, SDK class MockKAS(KAS): @@ -28,24 +19,6 @@ def get_key_cache(self): class MockServices(SDK.Services): - def attributes(self): - return AttributesServiceClientInterface() - - def namespaces(self): - return NamespaceServiceClientInterface() - - def subject_mappings(self): - return SubjectMappingServiceClientInterface() - - def resource_mappings(self): - return ResourceMappingServiceClientInterface() - - def authorization(self): - return AuthorizationServiceClientInterface() - - def kas_registry(self): - return KeyAccessServerRegistryServiceClientInterface() - def kas(self): return MockKAS()