Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion src/otdf_python/auth_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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
8 changes: 7 additions & 1 deletion src/otdf_python/kas_connect_rpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down
90 changes: 6 additions & 84 deletions src/otdf_python/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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:
"""
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
33 changes: 0 additions & 33 deletions src/otdf_python/sdk_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import ssl
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import httpx

Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand Down
21 changes: 21 additions & 0 deletions tests/test_inner_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 0 additions & 3 deletions tests/test_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 0 additions & 1 deletion tests/test_sdk_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 1 addition & 28 deletions tests/test_sdk_mock.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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()

Expand Down
Loading