From 18f5be87964dd75060bb3393069824aa0123d5fe Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:50:40 +0000 Subject: [PATCH 1/9] update clinet and test script --- .../azure/confidentialledger/_client.py | 2 +- .../azure-confidentialledger/test.py | 281 ++++++++++++++++++ 2 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 sdk/confidentialledger/azure-confidentialledger/test.py diff --git a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py index 5547a18f3e68..73925bfb3635 100644 --- a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py +++ b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py @@ -52,7 +52,7 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential self._config.custom_hook_policy, self._config.logging_policy, policies.DistributedTracingPolicy(**kwargs), - policies.SensitiveHeaderCleanupPolicy(**kwargs) if self._config.redirect_policy else None, + policies.SensitiveHeaderCleanupPolicy(disable_redirect_cleanup=True, **kwargs) if self._config.redirect_policy else None, self._config.http_logging_policy, ] self._client: PipelineClient = PipelineClient(base_url=_endpoint, policies=_policies, **kwargs) diff --git a/sdk/confidentialledger/azure-confidentialledger/test.py b/sdk/confidentialledger/azure-confidentialledger/test.py new file mode 100644 index 000000000000..d55790bd752d --- /dev/null +++ b/sdk/confidentialledger/azure-confidentialledger/test.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +""" +ACL Python SDK Redirect Behavior Test + +Tests whether the azure-confidentialledger Python SDK automatically follows +HTTP redirects for write operations (POST transactions). + +This test demonstrates: +1. PHASE 1: Default SDK behavior (fails due to Authorization header stripping on redirects) +2. PHASE 2: With disable_redirect_cleanup=True fix (succeeds) + +The key issue is that the SensitiveHeaderCleanupPolicy strips the Authorization header +when following redirects to a different hostname (the primary node), causing 401 errors. +""" + +import argparse +import logging +import os +import sys +import tempfile +import uuid +from datetime import datetime + +from azure.identity import DefaultAzureCredential +from azure.confidentialledger import ConfidentialLedgerClient +from azure.confidentialledger.certificate import ConfidentialLedgerCertificateClient + +# Default ACL endpoint +DEFAULT_ENDPOINT = "ap-redirect-test-4398.confidential-ledger.azure.com" +DEFAULT_IDENTITY_SERVICE_URL = "https://identity.confidential-ledger.core.azure.com" + +# Number of transactions to post (higher = more likely to hit redirect) +NUM_TRANSACTIONS = 5 + + +def setup_logging(verbose: bool): + """Configure logging level.""" + if verbose: + # Enable HTTP logging to see redirect chain + logging.basicConfig(level=logging.DEBUG, force=True) + # Azure SDK HTTP logging + logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel( + logging.DEBUG + ) + # Show redirect policy activity + logging.getLogger("azure.core.pipeline.policies._redirect").setLevel( + logging.DEBUG + ) + else: + logging.basicConfig(level=logging.WARNING, force=True) + + +def get_ledger_certificate(ledger_name: str, identity_service_url: str) -> str: + """Retrieve the ledger's TLS certificate from the identity service.""" + cert_client = ConfidentialLedgerCertificateClient( + certificate_endpoint=identity_service_url + ) + ledger_cert = cert_client.get_ledger_identity(ledger_name) + return ledger_cert["ledgerTlsCertificate"] + + +def get_cert_file(endpoint: str) -> str: + """Get ledger certificate and write to temp file. + + Returns: + str: Path to the certificate file + """ + # Extract ledger name from endpoint + ledger_name = endpoint.split(".")[0] + + # Determine identity service URL based on environment + if "staging" in endpoint or "confidential-ledger-staging" in endpoint: + identity_service_url = "https://identity.confidential-ledger-staging.core.azure.com" + else: + identity_service_url = DEFAULT_IDENTITY_SERVICE_URL + + # Get the ledger certificate + ledger_certificate = get_ledger_certificate(ledger_name, identity_service_url) + + # Write certificate to a temporary file (SDK requires file path) + cert_file = tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) + cert_file.write(ledger_certificate) + cert_file.close() + + return cert_file.name + + +def create_client(endpoint: str, cert_file_path: str) -> ConfidentialLedgerClient: + """Create an ACL client with specified configuration. + + Args: + endpoint: ACL endpoint hostname + cert_file_path: Path to the ledger certificate file + (Note: SDK now has this hardcoded to True) + + Returns: + ConfidentialLedgerClient instance + """ + # Create credential + credential = DefaultAzureCredential() + + # Create client + # NOTE: The SDK now has disable_redirect_cleanup=True hardcoded in _client.py + # to preserve the Authorization header when following redirects to the primary node. + # We no longer need to pass this parameter explicitly. + client = ConfidentialLedgerClient( + endpoint=f"https://{endpoint}", + credential=credential, + ledger_certificate_path=cert_file_path, + ) + + return client + + +def post_transaction(client: ConfidentialLedgerClient, index: int) -> tuple[bool, str]: + """ + Post a single transaction to the ledger. + + Returns: + tuple: (success: bool, message: str) + """ + # Generate unique content for this transaction + content = "test" + + try: + # Post the transaction (this is where redirect may happen) + result = client.create_ledger_entry( + entry={"contents": str(content)}, + collection_id="redirect-test" + ) + + transaction_id = result.get("transactionId", "unknown") + return True, f"transaction_id: {transaction_id}" + + except Exception as e: + error_msg = str(e) + # Check for common redirect-related failures + if "401" in error_msg or "Unauthorized" in error_msg: + return False, f"401 Unauthorized (likely Authorization header stripped on redirect)" + return False, f"Error: {error_msg}" + + +def run_phase(endpoint: str, cert_file_path: str, phase_name: str, verbose: bool) -> tuple[int, int]: + """ + Run a single test phase with specified configuration. + + Args: + endpoint: ACL endpoint hostname + cert_file_path: Path to the ledger certificate file + disable_redirect_cleanup: If True, preserves Authorization header on redirects + phase_name: Name of this phase for display + verbose: Enable verbose logging + + Returns: + tuple: (success_count, total_count) + """ + print() + print(f"{'─' * 60}") + print(f" {phase_name}") + print(f"{'─' * 60}") + + try: + client = create_client(endpoint, cert_file_path) + except Exception as e: + print(f" ERROR: Failed to create client: {e}") + return 0, NUM_TRANSACTIONS + + # Post transactions + success_count = 0 + for i in range(1, NUM_TRANSACTIONS + 1): + print(f" [{i}/{NUM_TRANSACTIONS}] Posting transaction... ", end="", flush=True) + success, message = post_transaction(client, i) + + if success: + print(f"✓ ({message})") + success_count += 1 + else: + print(f"✗ ({message})") + + return success_count, NUM_TRANSACTIONS + + +def run_test(endpoint: str, verbose: bool) -> bool: + """ + Run the redirect behavior test in two phases. + + Phase 1: Default SDK behavior (disable_redirect_cleanup=False) + Expected: FAIL - Authorization header stripped on redirects + + Phase 2: With fix (disable_redirect_cleanup=True) + Expected: PASS - Authorization header preserved + + Returns: + bool: True if Phase 2 succeeded (demonstrating the fix works) + """ + print("=" * 60) + print(" ACL Python SDK Redirect Test") + print("=" * 60) + print(f" Endpoint: https://{endpoint}") + print(f" Transactions per phase: {NUM_TRANSACTIONS}") + print() + print(" This test demonstrates the redirect Authorization header issue") + print(" and validates the fix using disable_redirect_cleanup=True.") + + # Get certificate once (shared between phases) + cert_file_path = None + try: + print() + print(" Retrieving ledger certificate from identity service...") + cert_file_path = get_cert_file(endpoint) + print(" Certificate retrieved successfully.") + except Exception as e: + print(f" ERROR: Failed to get certificate: {e}") + return False + + try: + # ═══════════════════════════════════════════════════════════════ + # PHASE 1: Default SDK behavior (should fail on redirects) + # NOTE: If requests go directly to primary node, this may PASS + # ═══════════════════════════════════════════════════════════════ + phase1_success, phase1_total = run_phase( + endpoint=endpoint, + cert_file_path=cert_file_path, + phase_name="PHASE 1: Default SDK Behavior (No Fix)", + verbose=verbose + ) + + # ═══════════════════════════════════════════════════════════════ + # RESULTS SUMMARY + # ═══════════════════════════════════════════════════════════════ + print() + print("=" * 60) + print(" RESULTS SUMMARY") + print("=" * 60) + print() + + phase1_passed = phase1_success == phase1_total + + # Phase 1 result + phase1_status = "PASS" if phase1_passed else "FAIL" + phase1_expected = "(may pass if no redirect)" if phase1_passed else "(expected on redirect)" + print(f" Phase 1 (Default): {phase1_status} {phase1_expected}") + print(f" {phase1_success}/{phase1_total} transactions succeeded") + + print() + print("─" * 60) + return phase1_passed + finally: + # Cleanup temp certificate file + if cert_file_path and os.path.exists(cert_file_path): + try: + os.unlink(cert_file_path) + except Exception: + pass # Ignore cleanup errors + + +def main(): + parser = argparse.ArgumentParser( + description="Test ACL Python SDK redirect behavior" + ) + parser.add_argument( + "--endpoint", + default=os.environ.get("ACL_ENDPOINT", DEFAULT_ENDPOINT), + help=f"ACL endpoint hostname (default: {DEFAULT_ENDPOINT})", + ) + parser.add_argument( + "--verbose", "-v", + action="store_true", + help="Enable verbose HTTP logging to see redirect chain", + ) + + args = parser.parse_args() + + setup_logging(args.verbose) + + success = run_test(args.endpoint, args.verbose) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() From 5e61ec9073ac191a894cba47d1a1d7e5997e2ce5 Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:47:58 +0000 Subject: [PATCH 2/9] remove test script --- .../azure-confidentialledger/test.py | 281 ------------------ 1 file changed, 281 deletions(-) delete mode 100644 sdk/confidentialledger/azure-confidentialledger/test.py diff --git a/sdk/confidentialledger/azure-confidentialledger/test.py b/sdk/confidentialledger/azure-confidentialledger/test.py deleted file mode 100644 index d55790bd752d..000000000000 --- a/sdk/confidentialledger/azure-confidentialledger/test.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env python3 -""" -ACL Python SDK Redirect Behavior Test - -Tests whether the azure-confidentialledger Python SDK automatically follows -HTTP redirects for write operations (POST transactions). - -This test demonstrates: -1. PHASE 1: Default SDK behavior (fails due to Authorization header stripping on redirects) -2. PHASE 2: With disable_redirect_cleanup=True fix (succeeds) - -The key issue is that the SensitiveHeaderCleanupPolicy strips the Authorization header -when following redirects to a different hostname (the primary node), causing 401 errors. -""" - -import argparse -import logging -import os -import sys -import tempfile -import uuid -from datetime import datetime - -from azure.identity import DefaultAzureCredential -from azure.confidentialledger import ConfidentialLedgerClient -from azure.confidentialledger.certificate import ConfidentialLedgerCertificateClient - -# Default ACL endpoint -DEFAULT_ENDPOINT = "ap-redirect-test-4398.confidential-ledger.azure.com" -DEFAULT_IDENTITY_SERVICE_URL = "https://identity.confidential-ledger.core.azure.com" - -# Number of transactions to post (higher = more likely to hit redirect) -NUM_TRANSACTIONS = 5 - - -def setup_logging(verbose: bool): - """Configure logging level.""" - if verbose: - # Enable HTTP logging to see redirect chain - logging.basicConfig(level=logging.DEBUG, force=True) - # Azure SDK HTTP logging - logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel( - logging.DEBUG - ) - # Show redirect policy activity - logging.getLogger("azure.core.pipeline.policies._redirect").setLevel( - logging.DEBUG - ) - else: - logging.basicConfig(level=logging.WARNING, force=True) - - -def get_ledger_certificate(ledger_name: str, identity_service_url: str) -> str: - """Retrieve the ledger's TLS certificate from the identity service.""" - cert_client = ConfidentialLedgerCertificateClient( - certificate_endpoint=identity_service_url - ) - ledger_cert = cert_client.get_ledger_identity(ledger_name) - return ledger_cert["ledgerTlsCertificate"] - - -def get_cert_file(endpoint: str) -> str: - """Get ledger certificate and write to temp file. - - Returns: - str: Path to the certificate file - """ - # Extract ledger name from endpoint - ledger_name = endpoint.split(".")[0] - - # Determine identity service URL based on environment - if "staging" in endpoint or "confidential-ledger-staging" in endpoint: - identity_service_url = "https://identity.confidential-ledger-staging.core.azure.com" - else: - identity_service_url = DEFAULT_IDENTITY_SERVICE_URL - - # Get the ledger certificate - ledger_certificate = get_ledger_certificate(ledger_name, identity_service_url) - - # Write certificate to a temporary file (SDK requires file path) - cert_file = tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) - cert_file.write(ledger_certificate) - cert_file.close() - - return cert_file.name - - -def create_client(endpoint: str, cert_file_path: str) -> ConfidentialLedgerClient: - """Create an ACL client with specified configuration. - - Args: - endpoint: ACL endpoint hostname - cert_file_path: Path to the ledger certificate file - (Note: SDK now has this hardcoded to True) - - Returns: - ConfidentialLedgerClient instance - """ - # Create credential - credential = DefaultAzureCredential() - - # Create client - # NOTE: The SDK now has disable_redirect_cleanup=True hardcoded in _client.py - # to preserve the Authorization header when following redirects to the primary node. - # We no longer need to pass this parameter explicitly. - client = ConfidentialLedgerClient( - endpoint=f"https://{endpoint}", - credential=credential, - ledger_certificate_path=cert_file_path, - ) - - return client - - -def post_transaction(client: ConfidentialLedgerClient, index: int) -> tuple[bool, str]: - """ - Post a single transaction to the ledger. - - Returns: - tuple: (success: bool, message: str) - """ - # Generate unique content for this transaction - content = "test" - - try: - # Post the transaction (this is where redirect may happen) - result = client.create_ledger_entry( - entry={"contents": str(content)}, - collection_id="redirect-test" - ) - - transaction_id = result.get("transactionId", "unknown") - return True, f"transaction_id: {transaction_id}" - - except Exception as e: - error_msg = str(e) - # Check for common redirect-related failures - if "401" in error_msg or "Unauthorized" in error_msg: - return False, f"401 Unauthorized (likely Authorization header stripped on redirect)" - return False, f"Error: {error_msg}" - - -def run_phase(endpoint: str, cert_file_path: str, phase_name: str, verbose: bool) -> tuple[int, int]: - """ - Run a single test phase with specified configuration. - - Args: - endpoint: ACL endpoint hostname - cert_file_path: Path to the ledger certificate file - disable_redirect_cleanup: If True, preserves Authorization header on redirects - phase_name: Name of this phase for display - verbose: Enable verbose logging - - Returns: - tuple: (success_count, total_count) - """ - print() - print(f"{'─' * 60}") - print(f" {phase_name}") - print(f"{'─' * 60}") - - try: - client = create_client(endpoint, cert_file_path) - except Exception as e: - print(f" ERROR: Failed to create client: {e}") - return 0, NUM_TRANSACTIONS - - # Post transactions - success_count = 0 - for i in range(1, NUM_TRANSACTIONS + 1): - print(f" [{i}/{NUM_TRANSACTIONS}] Posting transaction... ", end="", flush=True) - success, message = post_transaction(client, i) - - if success: - print(f"✓ ({message})") - success_count += 1 - else: - print(f"✗ ({message})") - - return success_count, NUM_TRANSACTIONS - - -def run_test(endpoint: str, verbose: bool) -> bool: - """ - Run the redirect behavior test in two phases. - - Phase 1: Default SDK behavior (disable_redirect_cleanup=False) - Expected: FAIL - Authorization header stripped on redirects - - Phase 2: With fix (disable_redirect_cleanup=True) - Expected: PASS - Authorization header preserved - - Returns: - bool: True if Phase 2 succeeded (demonstrating the fix works) - """ - print("=" * 60) - print(" ACL Python SDK Redirect Test") - print("=" * 60) - print(f" Endpoint: https://{endpoint}") - print(f" Transactions per phase: {NUM_TRANSACTIONS}") - print() - print(" This test demonstrates the redirect Authorization header issue") - print(" and validates the fix using disable_redirect_cleanup=True.") - - # Get certificate once (shared between phases) - cert_file_path = None - try: - print() - print(" Retrieving ledger certificate from identity service...") - cert_file_path = get_cert_file(endpoint) - print(" Certificate retrieved successfully.") - except Exception as e: - print(f" ERROR: Failed to get certificate: {e}") - return False - - try: - # ═══════════════════════════════════════════════════════════════ - # PHASE 1: Default SDK behavior (should fail on redirects) - # NOTE: If requests go directly to primary node, this may PASS - # ═══════════════════════════════════════════════════════════════ - phase1_success, phase1_total = run_phase( - endpoint=endpoint, - cert_file_path=cert_file_path, - phase_name="PHASE 1: Default SDK Behavior (No Fix)", - verbose=verbose - ) - - # ═══════════════════════════════════════════════════════════════ - # RESULTS SUMMARY - # ═══════════════════════════════════════════════════════════════ - print() - print("=" * 60) - print(" RESULTS SUMMARY") - print("=" * 60) - print() - - phase1_passed = phase1_success == phase1_total - - # Phase 1 result - phase1_status = "PASS" if phase1_passed else "FAIL" - phase1_expected = "(may pass if no redirect)" if phase1_passed else "(expected on redirect)" - print(f" Phase 1 (Default): {phase1_status} {phase1_expected}") - print(f" {phase1_success}/{phase1_total} transactions succeeded") - - print() - print("─" * 60) - return phase1_passed - finally: - # Cleanup temp certificate file - if cert_file_path and os.path.exists(cert_file_path): - try: - os.unlink(cert_file_path) - except Exception: - pass # Ignore cleanup errors - - -def main(): - parser = argparse.ArgumentParser( - description="Test ACL Python SDK redirect behavior" - ) - parser.add_argument( - "--endpoint", - default=os.environ.get("ACL_ENDPOINT", DEFAULT_ENDPOINT), - help=f"ACL endpoint hostname (default: {DEFAULT_ENDPOINT})", - ) - parser.add_argument( - "--verbose", "-v", - action="store_true", - help="Enable verbose HTTP logging to see redirect chain", - ) - - args = parser.parse_args() - - setup_logging(args.verbose) - - success = run_test(args.endpoint, args.verbose) - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - main() From 56cc74919c71eca4e624b1ce99dbcc6ce396438d Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:50:03 +0000 Subject: [PATCH 3/9] add comments --- .../azure/confidentialledger/_client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py index 73925bfb3635..92370f080501 100644 --- a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py +++ b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_client.py @@ -52,7 +52,13 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential self._config.custom_hook_policy, self._config.logging_policy, policies.DistributedTracingPolicy(**kwargs), - policies.SensitiveHeaderCleanupPolicy(disable_redirect_cleanup=True, **kwargs) if self._config.redirect_policy else None, + # Redirect cleanup is disabled to preserve authentication and ledger-specific headers + # on service-managed redirects. Confidential Ledger redirects are expected to stay within + # the same trusted ledger endpoint, so forwarding these sensitive headers is required + # for correct authentication behavior. + policies.SensitiveHeaderCleanupPolicy( + disable_redirect_cleanup=True, **kwargs + ) if self._config.redirect_policy else None, self._config.http_logging_policy, ] self._client: PipelineClient = PipelineClient(base_url=_endpoint, policies=_policies, **kwargs) From 94877a630ddc06d065ff8233eafdd22cce86a4f5 Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:57:33 +0000 Subject: [PATCH 4/9] add change log --- .../azure-confidentialledger/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md b/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md index 129dd85532b6..7e6115add04f 100644 --- a/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md +++ b/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 2.0.0b2 (Unreleased) + +### Bugs Fixed + +- Fixed authentication failure on HTTP redirects by preserving sensitive headers during service-managed redirects within the Confidential Ledger endpoint. + ## 2.0.0b1 (2025-10-20) ### Features Added From 44d00e6aa73e67120a826efcc17b2316d927df75 Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:09:43 +0000 Subject: [PATCH 5/9] update redirection policy --- .../azure/confidentialledger/aio/_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/aio/_client.py b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/aio/_client.py index a3e303ca7c68..7f49efc68499 100644 --- a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/aio/_client.py +++ b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/aio/_client.py @@ -52,7 +52,9 @@ def __init__( # pylint: disable=missing-client-constructor-parameter-credential self._config.custom_hook_policy, self._config.logging_policy, policies.DistributedTracingPolicy(**kwargs), - policies.SensitiveHeaderCleanupPolicy(**kwargs) if self._config.redirect_policy else None, + policies.SensitiveHeaderCleanupPolicy( + disable_redirect_cleanup=True, **kwargs + ) if self._config.redirect_policy else None, self._config.http_logging_policy, ] self._client: AsyncPipelineClient = AsyncPipelineClient(base_url=_endpoint, policies=_policies, **kwargs) From 2659ac7f65a58aa04793b51904e371deed2a225d Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:16:41 -0500 Subject: [PATCH 6/9] Update CHANGELOG for version 2.0.0b2 release Updated release date for version 2.0.0b2 and fixed authentication issue. --- sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md b/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md index 7e6115add04f..22366f06a123 100644 --- a/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md +++ b/sdk/confidentialledger/azure-confidentialledger/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 2.0.0b2 (Unreleased) +## 2.0.0b2 (2026-01-29) ### Bugs Fixed From 6a0543c75bc634d8761e02ed1b257f904e19d8f1 Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:28:16 +0000 Subject: [PATCH 7/9] add unit test --- .../tests/test_client_configuration.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 sdk/confidentialledger/azure-confidentialledger/tests/test_client_configuration.py diff --git a/sdk/confidentialledger/azure-confidentialledger/tests/test_client_configuration.py b/sdk/confidentialledger/azure-confidentialledger/tests/test_client_configuration.py new file mode 100644 index 000000000000..47459e6733dd --- /dev/null +++ b/sdk/confidentialledger/azure-confidentialledger/tests/test_client_configuration.py @@ -0,0 +1,98 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Unit tests for ConfidentialLedgerClient configuration.""" + +import pytest +from unittest.mock import patch, MagicMock +from azure.core.pipeline import policies + +# Import the generated client directly to test its policy configuration +from azure.confidentialledger._client import ConfidentialLedgerClient as GeneratedClient + + +class TestClientConfiguration: + """Tests for client configuration settings.""" + + def test_sensitive_header_cleanup_policy_disable_redirect_cleanup_enabled(self): + """Test that SensitiveHeaderCleanupPolicy has disable_redirect_cleanup=True. + + This ensures that authentication and ledger-specific headers are preserved + on service-managed redirects, which is required for correct authentication + behavior within the trusted Confidential Ledger endpoint. + """ + # Mock the PipelineClient to capture the policies passed to it + with patch("azure.confidentialledger._client.PipelineClient") as mock_pipeline_client: + mock_pipeline_client.return_value = MagicMock() + + # Create the generated client directly - this will trigger policy creation + # The generated client only requires ledger_endpoint + client = GeneratedClient( + ledger_endpoint="https://test-ledger.confidentialledger.azure.com" + ) + + # Get the policies argument passed to PipelineClient + call_args = mock_pipeline_client.call_args + policies_arg = call_args.kwargs.get("policies") or call_args[1].get("policies") + + # Find the SensitiveHeaderCleanupPolicy in the policies list + sensitive_header_policy = None + for policy in policies_arg: + if isinstance(policy, policies.SensitiveHeaderCleanupPolicy): + sensitive_header_policy = policy + break + + # Assert the policy exists and has disable_redirect_cleanup=True + assert sensitive_header_policy is not None, ( + "SensitiveHeaderCleanupPolicy should be present in the client's policies" + ) + assert sensitive_header_policy._disable_redirect_cleanup is True, ( + "SensitiveHeaderCleanupPolicy should have disable_redirect_cleanup=True " + "to preserve authentication headers on Confidential Ledger redirects" + ) + + client.close() + + def test_sensitive_header_cleanup_policy_is_in_correct_position(self): + """Test that SensitiveHeaderCleanupPolicy is positioned after authentication_policy. + + The policy should be placed after the authentication policy so that it can + properly handle the redirect cleanup for authentication headers. + """ + with patch("azure.confidentialledger._client.PipelineClient") as mock_pipeline_client: + mock_pipeline_client.return_value = MagicMock() + + client = GeneratedClient( + ledger_endpoint="https://test-ledger.confidentialledger.azure.com" + ) + + # Get the policies argument passed to PipelineClient + call_args = mock_pipeline_client.call_args + policies_arg = call_args.kwargs.get("policies") or call_args[1].get("policies") + + # Filter out None values + non_none_policies = [p for p in policies_arg if p is not None] + + # Find positions of key policies + sensitive_header_idx = None + distributed_tracing_idx = None + + for idx, policy in enumerate(non_none_policies): + if isinstance(policy, policies.SensitiveHeaderCleanupPolicy): + sensitive_header_idx = idx + elif isinstance(policy, policies.DistributedTracingPolicy): + distributed_tracing_idx = idx + + # SensitiveHeaderCleanupPolicy should come after DistributedTracingPolicy + assert sensitive_header_idx is not None, ( + "SensitiveHeaderCleanupPolicy should be present in the policies" + ) + assert distributed_tracing_idx is not None, ( + "DistributedTracingPolicy should be present in the policies" + ) + assert sensitive_header_idx > distributed_tracing_idx, ( + "SensitiveHeaderCleanupPolicy should be positioned after DistributedTracingPolicy" + ) + + client.close() From 72ac51db64abc53ad0bfeb672e8c52bd864eff97 Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:05:28 +0000 Subject: [PATCH 8/9] Update version to 2.0.0b2 --- .../azure/confidentialledger/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_version.py b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_version.py index 0e00a6283246..8eb37199ee54 100644 --- a/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_version.py +++ b/sdk/confidentialledger/azure-confidentialledger/azure/confidentialledger/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "2.0.0b1" +VERSION = "2.0.0b2" From 7b88872d2498c2beec976420e79cf8b78ab3ad5d Mon Sep 17 00:00:00 2001 From: Ryan Zhang <112638134+ryazhang-microsoft@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:07:24 +0000 Subject: [PATCH 9/9] Trigger CI rebuild