From 660319badb779456a3bd83827f21d6fde1f5c68b Mon Sep 17 00:00:00 2001 From: "Vinothini Dharmaraj (Centific Technologies Inc)" Date: Thu, 22 Oct 2026 17:06:40 -0700 Subject: [PATCH 1/3] fixing the security issue --- .../CHANGELOG.md | 10 + .../README.md | 31 ++ .../callautomation/_call_automation_client.py | 6 + .../callautomation/_content_downloader.py | 6 +- .../aio/_call_automation_client_async.py | 8 +- .../aio/_content_downloader_async.py | 6 +- .../callautomation/recording_url_validator.py | 49 +++- .../tests/test_recording_url_security.py | 264 ++++++++++++++++++ 8 files changed, 374 insertions(+), 6 deletions(-) create mode 100644 sdk/communication/azure-communication-callautomation/tests/test_recording_url_security.py diff --git a/sdk/communication/azure-communication-callautomation/CHANGELOG.md b/sdk/communication/azure-communication-callautomation/CHANGELOG.md index 6efa7852b286..e0c58cd6719f 100644 --- a/sdk/communication/azure-communication-callautomation/CHANGELOG.md +++ b/sdk/communication/azure-communication-callautomation/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 1.6.1 (Unreleased) + +### Bugs Fixed + +- Security: Hardened recording URL validation against ambiguous authorities that could send authenticated requests to unintended endpoints. Synchronous and asynchronous recording downloads and deletes now validate each redirect destination before authentication. + +### Other Changes + +- Deprecated version 1.6.0 as of 2026-10-22 due to the recording URL validation security issue. Upgrade to 1.6.1 or a later fixed release to receive the security fix. This deprecation applies to version 1.6.0 only, not to the library as a whole. + ## 1.6.0 (2026-06-24) ### Features Added diff --git a/sdk/communication/azure-communication-callautomation/README.md b/sdk/communication/azure-communication-callautomation/README.md index 88fe3cfd79e7..e99bff35c74f 100644 --- a/sdk/communication/azure-communication-callautomation/README.md +++ b/sdk/communication/azure-communication-callautomation/README.md @@ -8,6 +8,26 @@ This package contains a Python SDK for Azure Communication Call Automation. Call _Azure SDK Python packages support for Python 2.7 has ended 01 January 2022. For more information and questions, please refer to https://github.com/Azure/azure-sdk-for-python/issues/20691_ +## Security notice: version 1.6.0 deprecated + +Version **1.6.0** of `azure-communication-callautomation` is deprecated as of +**2026-10-22** because of a recording URL validation security issue. This notice +applies to version 1.6.0, not to the library as a whole. + +In applications that pass attacker-influenced recording URLs to the synchronous +`download_recording` or `delete_recording` methods using the default Requests +transport, an ambiguous URL can bypass hostname validation and send an +authenticated request to an unintended endpoint, potentially exposing a Bearer +token. + +Upgrade to **1.6.1** or a later fixed release to receive the security fix. +Use only service-generated recording URLs received through trusted, +integrity-protected events; do not pass untrusted recording URLs to these methods. + +Refer to the [release history](CHANGELOG.md) and +[Azure SDK support policy](https://azure.github.io/azure-sdk/policies_support.html) +for more information. + ## Getting started ### Prerequisites - Python 3.8 or later is required to use this package. @@ -29,6 +49,17 @@ pip install azure-communication-callautomation | Callback Events | Callback events are events sent back during duration of the call. It gives information and state of the call, such as `CallConnected`. `CallbackUrl` must be provided during `createCall` and `answerCall`, and callback events will be sent to this url. | | Incoming Call Event | When incoming call happens (that can be answered with `answerCall`), incoming call eventgrid event will be sent. This is different from Callback events above, and should be setup on Azure portal. See [Incoming Call][incomingcall] for detail. | +### Recording URL security + +`download_recording` and `delete_recording` accept only HTTPS URLs with ASCII DNS +hostnames under `*.asm.skype.com`, `*.asyncgw.teams.microsoft.com`, or +`*.ams.gov.teams.microsoft.us`. URLs containing user information, raw backslashes, +whitespace, control characters, or malformed authorities are rejected with +`ValueError` before authentication. Redirect destinations are subject to the same +validation. Valid signed query parameters are preserved. + +Use service-generated recording URLs from trusted, integrity-protected events. + ## Examples ### Initialize CallAutomationClient ```Python diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_call_automation_client.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_call_automation_client.py index 0ea1f72d63af..e546671f6d45 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_call_automation_client.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_call_automation_client.py @@ -15,6 +15,7 @@ from ._call_connection_client import CallConnectionClient from ._generated._client import AzureCommunicationCallAutomationService from ._shared.auth_policy_utils import get_authentication_policy +from .recording_url_validator import RecordingURLValidationPolicy from ._shared.utils import parse_connection_str from ._shared.models import CommunicationIdentifier from ._generated.models import ( @@ -107,6 +108,11 @@ def __init__( if not parsed_url.netloc: raise ValueError(f"Invalid URL: {format(endpoint)}") + per_retry_policies = kwargs.pop("per_retry_policies", None) or [] + if not isinstance(per_retry_policies, Iterable): + per_retry_policies = [per_retry_policies] + kwargs["per_retry_policies"] = [*per_retry_policies, RecordingURLValidationPolicy()] + self._client = AzureCommunicationCallAutomationService( endpoint, credential, # type: ignore[arg-type] diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py index e56a82408e16..be814802778b 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py @@ -66,7 +66,8 @@ def download_streaming( raise ValueError("Recording client endpoint must not be None.") # Validate recording URL before sending authenticated request - validate_recording_url(source_location, "source_location") + source_location = validate_recording_url(source_location, "source_location") + kwargs["_recording_url_parameter_name"] = "source_location" _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} @@ -116,7 +117,8 @@ def delete_recording(self, recording_location: str, **kwargs: Any) -> None: raise ValueError("Recording client endpoint must not be None.") # Validate recording URL before sending authenticated request - validate_recording_url(recording_location, "recording_location") + recording_location = validate_recording_url(recording_location, "recording_location") + kwargs["_recording_url_parameter_name"] = "recording_location" _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_call_automation_client_async.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_call_automation_client_async.py index f3c638b4423a..38b70cd27bfe 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_call_automation_client_async.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_call_automation_client_async.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from typing import Sequence, Union, Optional, TYPE_CHECKING, AsyncIterable, overload +from typing import Sequence, Union, Optional, TYPE_CHECKING, AsyncIterable, Iterable, overload from urllib.parse import urlparse import warnings @@ -15,6 +15,7 @@ from ._call_connection_client_async import CallConnectionClient from .._generated.aio import AzureCommunicationCallAutomationService from .._shared.auth_policy_utils import get_authentication_policy +from ..recording_url_validator import RecordingURLValidationPolicy from .._shared.utils import parse_connection_str from .._shared.models import CommunicationIdentifier from .._generated.models import ( @@ -108,6 +109,11 @@ def __init__( if not parsed_url.netloc: raise ValueError(f"Invalid URL: {format(endpoint)}") + per_retry_policies = kwargs.pop("per_retry_policies", None) or [] + if not isinstance(per_retry_policies, Iterable): + per_retry_policies = [per_retry_policies] + kwargs["per_retry_policies"] = [*per_retry_policies, RecordingURLValidationPolicy()] + self._client = AzureCommunicationCallAutomationService( endpoint, credential, # type: ignore[arg-type] diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py index a673cc26f027..9ace00411979 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py @@ -67,7 +67,8 @@ async def download_streaming( raise ValueError("Recording client endpoint must not be None.") # Validate recording URL before sending authenticated request - validate_recording_url(source_location, "source_location") + source_location = validate_recording_url(source_location, "source_location") + kwargs["_recording_url_parameter_name"] = "source_location" _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} @@ -118,7 +119,8 @@ async def delete_recording(self, recording_location: str, **kwargs: Any) -> None raise ValueError("Recording client endpoint must not be None.") # Validate recording URL before sending authenticated request - validate_recording_url(recording_location, "recording_location") + recording_location = validate_recording_url(recording_location, "recording_location") + kwargs["_recording_url_parameter_name"] = "recording_location" _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/recording_url_validator.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/recording_url_validator.py index b5ba09bff008..d2bf0dac7d6c 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/recording_url_validator.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/recording_url_validator.py @@ -4,9 +4,12 @@ # license information. # -------------------------------------------------------------------------- +import re from typing import Tuple from urllib.parse import urlparse +from azure.core.pipeline import PipelineRequest, PipelineResponse +from azure.core.pipeline.policies import SansIOHTTPPolicy # Allowed recording endpoint host suffixes. # These are the only domains permitted for recording URLs to prevent credential exfiltration. @@ -17,7 +20,12 @@ ) -def validate_recording_url(recording_url: str, parameter_name: str) -> None: +def _validate_url_characters(url: str, parameter_name: str) -> None: + if any(char == "\\" or char.isspace() or ord(char) < 32 or 127 <= ord(char) <= 159 for char in url): + raise ValueError(f"{parameter_name} must not contain backslashes, whitespace, or control characters.") + + +def validate_recording_url(recording_url: str, parameter_name: str) -> str: """ Validate that a recording URL points to Azure Communication Services endpoint before credentials are attached. @@ -27,12 +35,18 @@ def validate_recording_url(recording_url: str, parameter_name: str) -> None: :type recording_url: str :param parameter_name: The parameter name for exception messages. :type parameter_name: str + :return: The validated URL with a canonical ASCII authority. + :rtype: str :raises TypeError: If the recording URL is None or empty. :raises ValueError: If the recording URL is not a valid absolute HTTPS URI or not from an allowed domain. """ if not recording_url: raise TypeError(f"{parameter_name} cannot be null or undefined.") + if not isinstance(recording_url, str): + raise TypeError(f"{parameter_name} must be a string.") + # Reject syntax that URL parsers may discard or interpret as a different authority. + _validate_url_characters(recording_url, parameter_name) parsed_url = urlparse(recording_url) # Validate it's a valid absolute URI @@ -43,11 +57,22 @@ def validate_recording_url(recording_url: str, parameter_name: str) -> None: if parsed_url.scheme.lower() != "https": raise ValueError(f"{parameter_name} must use HTTPS scheme for security.") + if "@" in parsed_url.netloc or not parsed_url.netloc.isascii(): + raise ValueError(f"{parameter_name} must have an ASCII hostname and must not contain user information.") + host = parsed_url.hostname if not host: raise ValueError(f"{parameter_name} must have a valid hostname.") host_lower = host.lower() + if len(host_lower) > 253 or not all( + re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", label) for label in host_lower.split(".") + ): + raise ValueError(f"{parameter_name} must have a valid DNS hostname.") + + port = parsed_url.port + if ":" in parsed_url.netloc and not parsed_url.netloc.rsplit(":", 1)[1].isdigit(): + raise ValueError(f"{parameter_name} must have a valid port.") # Check against allowed suffixes is_valid_endpoint = any(host_lower.endswith(suffix) for suffix in ALLOWED_HOST_SUFFIXES) @@ -57,3 +82,25 @@ def validate_recording_url(recording_url: str, parameter_name: str) -> None: f"{parameter_name} host '{host}' is not a valid Azure Communication Services recording endpoint. " "Only URLs pointing to *.asm.skype.com, *.asyncgw.teams.microsoft.com are allowed." ) + + authority = host_lower if port is None else f"{host_lower}:{port}" + return parsed_url._replace(scheme="https", netloc=authority).geturl() + + +class RecordingURLValidationPolicy(SansIOHTTPPolicy): + """Validate recording destinations on every attempt, before authentication.""" + + def on_request(self, request: PipelineRequest) -> None: + parameter_name = request.context.options.pop("_recording_url_parameter_name", None) + if parameter_name is not None: + request.context["_recording_url_parameter_name"] = parameter_name + parameter_name = request.context.get("_recording_url_parameter_name") + if parameter_name: + request.http_request.url = validate_recording_url(request.http_request.url, parameter_name) + + def on_response(self, request: PipelineRequest, response: PipelineResponse) -> None: + parameter_name = request.context.get("_recording_url_parameter_name") + location = response.http_response.headers.get("location") + if parameter_name and location and response.http_response.status_code in (300, 301, 302, 303, 307, 308): + # RedirectPolicy parses Location before the next on_request, potentially discarding controls. + _validate_url_characters(location, parameter_name) diff --git a/sdk/communication/azure-communication-callautomation/tests/test_recording_url_security.py b/sdk/communication/azure-communication-callautomation/tests/test_recording_url_security.py new file mode 100644 index 000000000000..519ea54bb950 --- /dev/null +++ b/sdk/communication/azure-communication-callautomation/tests/test_recording_url_security.py @@ -0,0 +1,264 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import time +from io import BytesIO +from unittest.mock import AsyncMock, MagicMock, Mock +from urllib.parse import urlparse + +import pytest +import requests +from azure.core.credentials import AccessToken +from azure.core.pipeline.transport import AioHttpTransport, RequestsTransport +from azure.communication.callautomation import CallAutomationClient +from azure.communication.callautomation.aio import CallAutomationClient as AsyncCallAutomationClient +from azure.communication.callautomation.recording_url_validator import validate_recording_url + +from unittest_helpers import mock_response + +ENDPOINT = "https://example.communication.azure.com" +VALID_URL = "https://x.asm.skype.com/recording?sig=a%2Fb%3D&part=1" +BYPASS_URL = "https://127.0.0.1:8443\\@x.asm.skype.com/recording" +OPERATIONS = ("download_recording", "delete_recording") +INVALID_URLS = [ + BYPASS_URL, + "https://127.0.0.1:8443\\\\@x.asm.skype.com/recording", + "https://user:password@x.asm.skype.com/recording", + "https://127.0.0.1:8443/recording", + "https://x.asm.skype.com.attacker.example/recording", + "https://asm.skype.com/recording", + "http://x.asm.skype.com/recording", + "//x.asm.skype.com/recording", + "https://x.asm.skype.com:/recording", + "https://x.asm.skype.com:invalid/recording", + "https://x.asm.skype.com:65536/recording", + "https://%78.asm.skype.com/recording", + "https://bad_host.asm.skype.com/recording", + "https://x..asm.skype.com/recording", + "https://[x.asm.skype.com]/recording", + "https://\u212a.asm.skype.com/recording", +] + [f"https://x.asm.skype.com/record{chr(code)}ing" for code in (*range(33), 127, 133)] + + +@pytest.fixture(autouse=True) +def start_proxy(): + # These tests must not start the service test proxy or make network requests. + return + + +def credential(async_client=False): + get_token = AsyncMock if async_client else Mock + return Mock( + spec=["get_token"], + get_token=get_token(return_value=AccessToken("ACS-CALLAUTOMATION-CANARY-TOKEN", int(time.time()) + 3600)), + ) + + +@pytest.mark.parametrize("operation", OPERATIONS) +def test_rejects_backslash_before_authentication(operation): + token = credential() + transport = MagicMock(send=Mock(return_value=mock_response())) + with CallAutomationClient(ENDPOINT, token, transport=transport) as client: + with pytest.raises(ValueError): + getattr(client, operation)(BYPASS_URL) + token.get_token.assert_not_called() + transport.send.assert_not_called() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("operation", OPERATIONS) +async def test_async_rejects_backslash_before_authentication(operation): + token = credential(async_client=True) + transport = MagicMock(send=AsyncMock(return_value=mock_response())) + async with AsyncCallAutomationClient(ENDPOINT, token, transport=transport) as client: + with pytest.raises(ValueError): + await getattr(client, operation)(BYPASS_URL) + token.get_token.assert_not_called() + transport.send.assert_not_called() + + +@pytest.mark.parametrize("url", INVALID_URLS) +def test_rejects_ambiguous_or_external_url(url): + with pytest.raises(ValueError): + validate_recording_url(url, "recording_url") + + +@pytest.mark.parametrize( + "url", + [ + VALID_URL, + "https://x.asyncgw.teams.microsoft.com/recording", + "https://x.ams.gov.teams.microsoft.us/recording", + "https://xn--bcher-kva.asm.skype.com/recording", + "https://x.asm.skype.com:8443/recording", + "https://x.asm.skype.com/recording%5C%40example?sig=a%2Fb%3D", + ], +) +def test_preserves_valid_recording_url(url): + assert validate_recording_url(url, "recording_url") == url + + +def test_canonicalizes_authority_without_changing_signature(): + url = "HTTPS://X.ASM.SKYPE.COM:443/Recording;param?sig=a%2Fb%3D#fragment" + assert validate_recording_url(url, "recording_url") == ( + "https://x.asm.skype.com:443/Recording;param?sig=a%2Fb%3D#fragment" + ) + + +@pytest.mark.parametrize("operation", OPERATIONS) +@pytest.mark.parametrize( + "location", + [ + BYPASS_URL, + "https://attacker.example/recording", + "//attacker.example/recording", + "http://x.asm.skype.com/recording", + "https://user@x.asm.skype.com/recording", + "https://x.asm.skype.com/record\ning", + ], +) +def test_rejects_unsafe_redirect(operation, location): + transport = MagicMock(send=Mock(return_value=mock_response(307, {"location": location}))) + with CallAutomationClient(ENDPOINT, credential(), transport=transport) as client: + with pytest.raises(ValueError): + getattr(client, operation)(VALID_URL, permit_redirects=True) + assert transport.send.call_count == 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("operation", OPERATIONS) +@pytest.mark.parametrize("location", [BYPASS_URL, "//attacker.example/recording", "https://x.asm.skype.com/a\tb"]) +async def test_async_rejects_unsafe_redirect(operation, location): + transport = MagicMock(send=AsyncMock(return_value=mock_response(307, {"location": location}))) + async with AsyncCallAutomationClient(ENDPOINT, credential(async_client=True), transport=transport) as client: + with pytest.raises(ValueError): + await getattr(client, operation)(VALID_URL, permit_redirects=True) + assert transport.send.call_count == 1 + + +@pytest.mark.parametrize("operation", OPERATIONS) +@pytest.mark.parametrize("location", ["/next?sig=a%2Fb", "https://other.asm.skype.com/next?sig=a%2Fb"]) +def test_preserves_allowed_redirects(operation, location): + urls = [] + + def send(request, **kwargs): + urls.append(request.url) + return mock_response(307, {"location": location}) if len(urls) == 1 else mock_response() + + with CallAutomationClient(ENDPOINT, credential(), transport=MagicMock(send=send)) as client: + getattr(client, operation)(VALID_URL) + expected = "https://x.asm.skype.com" + location if location.startswith("/") else location + assert urls == [VALID_URL, expected] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("operation", OPERATIONS) +@pytest.mark.parametrize("location", ["/next?sig=a%2Fb", "https://other.asm.skype.com/next?sig=a%2Fb"]) +async def test_async_preserves_allowed_redirects(operation, location): + urls = [] + + async def send(request, **kwargs): + urls.append(request.url) + return mock_response(307, {"location": location}) if len(urls) == 1 else mock_response() + + transport = MagicMock(send=AsyncMock(side_effect=send)) + async with AsyncCallAutomationClient(ENDPOINT, credential(async_client=True), transport=transport) as client: + await getattr(client, operation)(VALID_URL) + expected = "https://x.asm.skype.com" + location if location.startswith("/") else location + assert urls == [VALID_URL, expected] + + +class CaptureAdapter(requests.adapters.BaseAdapter): + def __init__(self, redirect_url=None): + self.requests = [] + self.redirect_url = redirect_url + + def send(self, request, **kwargs): + self.requests.append(request) + if self.redirect_url and len(self.requests) == 1: + response = requests.Response() + response.status_code = 307 + response.headers["location"] = self.redirect_url + response.raw = BytesIO(b"") + response._content = b"" + response.request = request + response.url = request.url + return response + raise AssertionError("No network request should be made") + + def close(self): + pass + + +@pytest.mark.parametrize("operation", OPERATIONS) +@pytest.mark.parametrize("url", INVALID_URLS) +def test_requests_transport_rejects_invalid_url(operation, url): + adapter = CaptureAdapter() + token = credential() + with requests.Session() as session: + session.mount("https://", adapter) + with CallAutomationClient(ENDPOINT, token, transport=RequestsTransport(session=session)) as client: + with pytest.raises(ValueError): + getattr(client, operation)(url) + assert adapter.requests == [] + token.get_token.assert_not_called() + + +@pytest.mark.parametrize("operation", OPERATIONS) +@pytest.mark.parametrize("location", [BYPASS_URL, "https://attacker.example/recording"]) +def test_requests_transport_rejects_unsafe_redirect(operation, location): + adapter = CaptureAdapter(redirect_url=location) + with requests.Session() as session: + session.mount("https://", adapter) + with CallAutomationClient(ENDPOINT, credential(), transport=RequestsTransport(session=session)) as client: + with pytest.raises(ValueError): + getattr(client, operation)(VALID_URL) + assert len(adapter.requests) == 1 + assert adapter.requests[0].url == VALID_URL + + +@pytest.mark.parametrize("operation", OPERATIONS) +def test_requests_transport_uses_validated_destination(operation): + adapter = CaptureAdapter() + with requests.Session() as session: + session.mount("https://", adapter) + with CallAutomationClient(ENDPOINT, credential(), transport=RequestsTransport(session=session)) as client: + with pytest.raises(AssertionError, match="No network request"): + getattr(client, operation)(VALID_URL) + assert len(adapter.requests) == 1 + assert adapter.requests[0].url == VALID_URL + assert adapter.requests[0].headers["Authorization"] == "Bearer ACS-CALLAUTOMATION-CANARY-TOKEN" + assert urlparse(adapter.requests[0].url).hostname == "x.asm.skype.com" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("operation", OPERATIONS) +@pytest.mark.parametrize("url", INVALID_URLS + [VALID_URL]) +async def test_aiohttp_transport_destination(operation, url): + import aiohttp + + destinations = [] + trace = aiohttp.TraceConfig() + + async def on_request_start(session, context, params): + destinations.append(params) + raise AssertionError("Stop before network I/O") + + trace.on_request_start.append(on_request_start) + token = credential(async_client=True) + async with aiohttp.ClientSession(trace_configs=[trace]) as session: + transport = AioHttpTransport(session=session, session_owner=False) + async with AsyncCallAutomationClient(ENDPOINT, token, transport=transport) as client: + if url != VALID_URL: + with pytest.raises(ValueError): + await getattr(client, operation)(url) + assert destinations == [] + token.get_token.assert_not_called() + else: + with pytest.raises(AssertionError, match="Stop before network"): + await getattr(client, operation)(url) + assert len(destinations) == 1 + assert destinations[0].url.host == "x.asm.skype.com" + assert destinations[0].headers["Authorization"] == "Bearer ACS-CALLAUTOMATION-CANARY-TOKEN" From 13d7c7bda2b41b835984659376c2485d577d1821 Mon Sep 17 00:00:00 2001 From: "Vinothini Dharmaraj (Centific Technologies Inc)" Date: Mon, 26 Oct 2026 13:15:01 -0700 Subject: [PATCH 2/3] updating the release date and version number --- .../azure-communication-callautomation/CHANGELOG.md | 2 +- .../azure/communication/callautomation/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/communication/azure-communication-callautomation/CHANGELOG.md b/sdk/communication/azure-communication-callautomation/CHANGELOG.md index e0c58cd6719f..c58577abd743 100644 --- a/sdk/communication/azure-communication-callautomation/CHANGELOG.md +++ b/sdk/communication/azure-communication-callautomation/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.6.1 (Unreleased) +## 1.6.1 (2026-10-25) ### Bugs Fixed diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_version.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_version.py index afb4e6136dcd..7eec7b3a1cdf 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_version.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_version.py @@ -4,6 +4,6 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "1.6.0" +VERSION = "1.6.1" SDK_MONIKER = f"communication-callautomation/{format(VERSION)}" From 4ced90a49aea8409160cda8ea83dea883d61de50 Mon Sep 17 00:00:00 2001 From: "Vinothini Dharmaraj (Centific Technologies Inc)" Date: Mon, 26 Oct 2026 16:14:28 -0700 Subject: [PATCH 3/3] fixing the docs pipeline failure --- sdk/communication/azure-communication-callautomation/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/communication/azure-communication-callautomation/README.md b/sdk/communication/azure-communication-callautomation/README.md index e99bff35c74f..f97958cdc43b 100644 --- a/sdk/communication/azure-communication-callautomation/README.md +++ b/sdk/communication/azure-communication-callautomation/README.md @@ -24,7 +24,7 @@ Upgrade to **1.6.1** or a later fixed release to receive the security fix. Use only service-generated recording URLs received through trusted, integrity-protected events; do not pass untrusted recording URLs to these methods. -Refer to the [release history](CHANGELOG.md) and +Refer to the [release history](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/communication/azure-communication-callautomation/CHANGELOG.md) and [Azure SDK support policy](https://azure.github.io/azure-sdk/policies_support.html) for more information.