diff --git a/changelog.md b/changelog.md index 1d0fb341..2d014517 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,40 @@ +# Microsoft 365 Agents SDK for Python - Release Notes v1.2.0 + +**Release Date:** 2026-07-17 +**Previous Version:** 1.1.0 (Released 2026-06-19) + +This release adds first-class Microsoft Teams hosting APIs and a credential-free Entra ID authentication provider, while improving turn processing, OAuth token handling, streaming responses, and async runtime behavior. + +## Major Features & Enhancements + +- **Teams Hosting Package**: Added the new `microsoft-agents-hosting-msteams` package for building Teams agents with typed, decorator-based routing. It introduces `TeamsAgentExtension`, `TeamsTurnContext`, route namespaces for Teams invoke and event activities, and Teams API client access from handlers (#434) +- **Entra Auth Sidecar Package**: Added the new `microsoft-agents-authentication-entra-auth-sidecar` package for credential-free Entra ID Agent ID authentication through the Microsoft Entra ID Agent Container sidecar. It includes `SidecarAuth`, `SidecarHttpClient`, `SidecarConnectionSettings`, in-memory token caching, transient retry handling, and SSRF-safe sidecar URL validation (#424) +- **AgentApplication Lifecycle Hooks**: Added `before_turn` and `after_turn` hooks for app-level logic around route execution without requiring full middleware. Hooks can short-circuit processing, participate in state saving, and support scenarios such as app-level guards, telemetry enrichment, and turn cleanup (#431) +- **AgentApplication Routing and Connections**: Improved route registration and exposed `AgentApplication.connection_manager` for code that needs access to configured connections (#429, #432) +- **Detached SSO Token Exchange**: Teams SSO token exchange invoke activities are now handled independently from regular route dispatch, reducing duplicate route logic and making SSO callbacks more reliable (#469) + +## New Models & APIs + +- **StreamingResponse Attachments and Reset**: Added `StreamingResponse.add_attachment()` for final-message attachments and `StreamingResponse.reset()` for returning the helper to its initial state after a stream completes (#426) +- **Activity Method Type Handling**: Improved `Activity` helper methods for typed activity data and entity conversions, including support for the new `ActivityTreatment` entity type (#441) +- **Configuration Coercion Helpers**: Added shared boolean, integer, and float coercion helpers so environment-derived configuration values are interpreted consistently (#424) + +## Bug Fixes + +- **State Loading**: Removed duplicate `turn_state.load()` calls and corrected when state is loaded during `AgentApplication` initialization (#455, #460) +- **Middleware Pipeline**: Fixed `MiddlewareSet` registration and turn invocation so multiple middleware components are registered and invoked in order (#461) +- **Composite Channels**: Normalized composite channel IDs for all user token operations, improving OAuth behavior for Copilot Web and Teams channel IDs such as `msteams:copilot-web` (#457) +- **Teams Activity Hooks**: Fixed `TeamsActivityHandler` hook dispatch so Teams-specific handlers receive the required arguments (#450) +- **Concurrency**: Replaced runtime use of `threading.Lock` with `asyncio.Lock` in async execution paths to avoid blocking the event loop (#442) +- **Dependencies**: Removed the unused `azure-core` dependency from `microsoft-agents-hosting-core` (#467) + +## Developer Experience + +- **Logging**: Added color-coded log messages and simplified `ApplicationOptions` by removing the logger option in favor of standard module-level logging (#439, #443) +- **Samples and Documentation**: Fixed the FastAPI sample startup path and added missing dialogs package links to README release tables (#417, #458) + +--- + # Microsoft 365 Agents SDK for Python - Release Notes v1.1.0 **Release Date:** 2026-06-19 @@ -16,8 +53,6 @@ ## Developer Experience -- **Integration Testing**: Integration testing enhancements (#408) -- **Testing Initialization Helper**: New testing initialization helper (#416) - **Typing Annotations**: Updated typing annotations across the codebase (#418) --- @@ -25,12 +60,11 @@ # Microsoft 365 Agents SDK for Python - Release Notes v1.0.0 **Release Date:** May 22, 2026 -**Previous Version:** 0.9.0 (Released April 15, 2026) +**Previous Version:** 0.9.1 (Released May 4, 2026) ## Major Features & Enhancements - **Dialogs Library**: New `microsoft-agents-hosting-dialogs` package porting the BotFramework dialog system (WaterfallDialog, ComponentDialog, DialogSet, DialogContext) and prompts (TextPrompt, NumberPrompt, ChoicePrompt, ConfirmPrompt, DateTimePrompt, AttachmentPrompt, OAuthPrompt) to the new SDK (#378) -- **Teams SSO Consent**: Support for the Teams SSO `Consent Required` flow (#380) - **Teams Extension Updates**: `TeamsAgentExtension` and related types re-exported at the `microsoft_agents.hosting.teams` package root, integration with `microsoft-teams-api` 2.x Pydantic models, and new runnable samples for message extensions, task modules, and meeting events (#385) - **Copilot Web OAuth Base-Channel Support**: New optional `force_base_channel` parameter on `Activity.get_conversation_reference()` and `_OAuthFlow.begin_flow()` so OAuth token exchange uses the base channel (e.g. `msteams`) when receiving composite channel IDs like `msteams:copilot-web` (#392) - **Per-Channel Typing Indicator**: New `TypingChannelStrategy` and `TypingOptions` for configurable per-channel typing behavior, surfaced via `ApplicationOptions` and a streaming fix (#398) @@ -39,9 +73,18 @@ ## Developer Experience - **API Reference Docstrings**: Docstrings updated from short-form to fully qualified object names for Learn API reference generation (#381) + +--- + +# Microsoft 365 Agents SDK for Python - Release Notes v0.9.1 + +**Release Date:** 2026-05-04 +**Previous Version:** 0.9.0 (Released 2026-04-15) + +## Enhancements + +- **Teams SSO Consent**: Support for the Teams SSO `Consent Required` flow (#380) - **SuggestedActions Factory**: New factory for the `to` property of `SuggestedActions` (#370) -- **Dialogs Test Layout**: `hosting-dialogs` tests moved back under the top-level `tests/` directory for consistency with other packages (#379) -- **Build Dependencies**: Temporary fix for build testing dependencies (#404) --- diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py index ef0de9f3..9d54c740 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/config/_load_configuration.py @@ -33,5 +33,5 @@ def load_configuration_from_env(env_vars: dict[str, Any]) -> dict: return { "AGENTAPPLICATION": result.get("AGENTAPPLICATION", {}), "CONNECTIONS": result.get("CONNECTIONS", {}), - "CONNECTIONSMAP": result.get("CONNECTIONSMAP", {}), + "CONNECTIONSMAP": result.get("CONNECTIONSMAP", []), } diff --git a/libraries/microsoft-agents-activity/readme.md b/libraries/microsoft-agents-activity/readme.md index 2b7d3e77..fc7ccb30 100644 --- a/libraries/microsoft-agents-activity/readme.md +++ b/libraries/microsoft-agents-activity/readme.md @@ -15,6 +15,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -33,6 +42,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-authentication-entra-auth-sidecar/readme.md b/libraries/microsoft-agents-authentication-entra-auth-sidecar/readme.md index b113baa0..b01260bf 100644 --- a/libraries/microsoft-agents-authentication-entra-auth-sidecar/readme.md +++ b/libraries/microsoft-agents-authentication-entra-auth-sidecar/readme.md @@ -7,6 +7,25 @@ Entra ID Agent Container** (the *sidecar*). Instead of acquiring tokens directly the SDK delegates token acquisition to the sidecar's HTTP API, so the agent process never handles secrets, certificates, or keys. +## Release Notes + + + + + + + + + + + + +
VersionDateRelease Notes
1.2.02026-07-17 + + 1.2.0 Release Notes + +
+ ## Why a sidecar? - **Credential-free agent code** — all credential management (Managed Identity, Workload diff --git a/libraries/microsoft-agents-authentication-msal/readme.md b/libraries/microsoft-agents-authentication-msal/readme.md index 76cfcc39..44087d89 100644 --- a/libraries/microsoft-agents-authentication-msal/readme.md +++ b/libraries/microsoft-agents-authentication-msal/readme.md @@ -15,6 +15,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -33,6 +42,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-copilotstudio-client/readme.md b/libraries/microsoft-agents-copilotstudio-client/readme.md index e57d135c..ed014f81 100644 --- a/libraries/microsoft-agents-copilotstudio-client/readme.md +++ b/libraries/microsoft-agents-copilotstudio-client/readme.md @@ -16,6 +16,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -34,6 +43,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-hosting-aiohttp/readme.md b/libraries/microsoft-agents-hosting-aiohttp/readme.md index e6a8e624..a882dc75 100644 --- a/libraries/microsoft-agents-hosting-aiohttp/readme.md +++ b/libraries/microsoft-agents-hosting-aiohttp/readme.md @@ -17,6 +17,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -35,6 +44,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 335d5b4f..51cc75bc 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -93,16 +93,15 @@ def __init__( ) if not auth_handlers: # get from config - handlers_config: dict[str, dict] = auth_configuration.get("HANDLERS") - if not auth_handlers and handlers_config: - auth_handlers = { - handler_name: AuthHandler( - name=handler_name, - auth_type=config.get("TYPE", None), - **config.get("SETTINGS", {}), - ) - for handler_name, config in handlers_config.items() - } + handlers_config: dict[str, dict] = auth_configuration.get("HANDLERS") or {} + auth_handlers = { + handler_name: AuthHandler( + name=handler_name, + auth_type=config.get("TYPE", ""), + **config.get("SETTINGS", {}), + ) + for handler_name, config in handlers_config.items() + } self._handler_settings = auth_handlers self._auto_sign_in = auto_sign_in or bool( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/_log_config.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/_log_config.py new file mode 100644 index 00000000..9f953ea3 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/_log_config.py @@ -0,0 +1,172 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +from logging import Logger +from urllib.parse import urlparse + +from microsoft_agents.activity._model_utils import pick_model_dict, SkipNone + +from .agent_auth_configuration import AgentAuthConfiguration + +_REDACTION_PEEK_LENGTH = 3 +_REDACTION_THRESH = _REDACTION_PEEK_LENGTH + 6 + + +def _redact_str(s: str, peek: bool = False) -> str: + """Redact a string for logging purposes. + + :arg s: The string to redact. + :type s: str + :arg peek: Whether to show a peek of the string. Defaults to False. + :type peek: bool + :return: The redacted string. + """ + if peek and len(s) > _REDACTION_THRESH: + return f"{s[:_REDACTION_PEEK_LENGTH]}..." + else: + return "..." + + +def _redact_str_or_none(s: str | None, peek: bool = False) -> str | None: + """Redact a string or None for logging purposes. + + :arg s: The string to redact or None. + :type s: str | None + :arg peek: Whether to show a peek of the string. Defaults to False. + :type peek: bool + :return: The redacted string or None. + :rtype: str | None + """ + if s is None: + return None + return _redact_str(s, peek=peek) + + +def _redact_scopes(scopes: list[str] | None) -> str | None: + """Redact a list of scopes for logging purposes. + + :arg scopes: The list of scopes to redact. + :type scopes: list[str] | None + :return: A string summarizing the scopes. + :rtype: str | None + """ + if scopes is None: + return None + return f"... [{len(scopes)} scope(s)]" + + +def _redact_url(url: str) -> str: + """ + Redact a URL for logging purposes. + + :arg url: The URL to redact. + :type url: str + :return: The redacted URL. + :rtype: str + """ + url = url.strip() + if not url: + return "" + + try: + url_parsed = urlparse(url) + return f"{url_parsed.scheme}://{url_parsed.netloc}/..." + except Exception: + return "..." + + +def _redact_url_or_none(url: str | None) -> str | None: + """Redact a URL or None for logging purposes. + + :arg url: The URL to redact or None. + :type url: str | None + :return: The redacted URL or None. + :rtype: str | None + """ + if url is None: + return None + return _redact_url(url) + + +def _summarize_auth_configs(config_map: dict[str, AgentAuthConfiguration]) -> str: + """ + Summarize the authentication configuration for logging. + + :arg config_map: A dictionary of connection configurations. + :type config_map: dict[str, :class:`microsoft_agents.hosting.core.AgentAuthConfiguration`] + :return: A string summarizing the authentication configuration. + :rtype: str + """ + summary = [] + for connection_name, config in config_map.items(): + summary.append( + pick_model_dict( + CONNECTION=connection_name, + CONNECTION_NAME=SkipNone(config.CONNECTION_NAME), + CLIENTID=SkipNone(_redact_str_or_none(config.CLIENT_ID, peek=True)), + TENANTID=SkipNone(_redact_str_or_none(config.TENANT_ID, peek=True)), + CLIENTSECRET=SkipNone(_redact_str_or_none(config.CLIENT_SECRET)), + AUTHORITY=SkipNone(_redact_url_or_none(config.AUTHORITY)), + SCOPES=SkipNone(_redact_scopes(config.SCOPES)), + FEDERATED_CLIENT_ID=SkipNone( + _redact_str_or_none(config.FEDERATED_CLIENT_ID, peek=True) + ), + CERT_PFX_FILE=SkipNone(_redact_str_or_none(config.CERT_PFX_FILE)), + ALT_BLUEPRINT_ID=SkipNone( + _redact_str_or_none(config.ALT_BLUEPRINT_ID, peek=True) + ), + IDPM_RESOURCE=SkipNone(_redact_url_or_none(config.IDPM_RESOURCE)), + AZURE_REGION=SkipNone(_redact_url_or_none(config.AZURE_REGION)), + ANONYMOUS_ALLOWED=str(config.ANONYMOUS_ALLOWED), + ) + ) + return json.dumps(summary, indent=2) + + +def _summarize_connections_map(connections_map: list[dict[str, str]]) -> str: + connections_map_output = [] + for mapping in connections_map: + obj = { + "CONNECTION": mapping.get("CONNECTION", ""), + } + + if "AUDIENCE" in mapping: + obj["AUDIENCE"] = mapping["AUDIENCE"] + + if "SERVICEURL" in mapping: + service_url = mapping.get("SERVICEURL", "").strip() + if service_url != "*": + service_url = _redact_url(service_url) + obj["SERVICEURL"] = service_url + + connections_map_output.append(obj) + + return json.dumps(connections_map_output, indent=2) + + +def _log_config( + logger: Logger, + config_map: dict[str, AgentAuthConfiguration], + connections_map: list[dict[str, str]], +) -> None: + """ + Log the configuration of the MSAL connection manager. + + :arg logger: The logger to use for logging. + :type logger: :class:`logging.Logger` + :arg connections_map: A list of connection mappings. + :type connections_map: list[dict[str, str]] + :arg config_map: A dictionary of connection configurations. + :type config_map: dict[str, :class:`microsoft_agents.hosting.core.AgentAuthConfiguration`] + """ + + connections_output = _summarize_auth_configs(config_map) + connections_map_output = _summarize_connections_map(connections_map) + + logger.info( + "\nConnections:\n%s\n\nConnections Map:\n%s", + connections_output, + connections_map_output, + ) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py index a1ac1b88..3efb71fc 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/connection_manager.py @@ -2,6 +2,8 @@ # Licensed under the MIT License. import re +import logging + from collections.abc import Callable from .agent_auth_configuration import AgentAuthConfiguration @@ -9,6 +11,10 @@ from .claims_identity import ClaimsIdentity from .connections import Connections +from ._log_config import _log_config + +logger = logging.getLogger(__name__) + class ConnectionManager(Connections): """ @@ -59,6 +65,7 @@ def __init__( if connections_map is not None else kwargs.get("CONNECTIONSMAP", []) ) + if isinstance(raw_connections_map, dict): raw_connections_map = list(raw_connections_map.values()) self._connections_map = raw_connections_map or [] @@ -89,6 +96,8 @@ def __init__( if not self._connections.get("SERVICE_CONNECTION", None): raise ValueError("No service connection configuration provided.") + _log_config(logger, self._config_map, self._connections_map) + def get_connection(self, connection_name: str | None) -> AccessTokenProviderBase: """ Get the OAuth connection for the agent. diff --git a/libraries/microsoft-agents-hosting-core/readme.md b/libraries/microsoft-agents-hosting-core/readme.md index 3078b97d..04109423 100644 --- a/libraries/microsoft-agents-hosting-core/readme.md +++ b/libraries/microsoft-agents-hosting-core/readme.md @@ -16,6 +16,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -34,6 +43,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-hosting-dialogs/readme.md b/libraries/microsoft-agents-hosting-dialogs/readme.md index 98630500..a84d4a39 100644 --- a/libraries/microsoft-agents-hosting-dialogs/readme.md +++ b/libraries/microsoft-agents-hosting-dialogs/readme.md @@ -64,6 +64,15 @@ dialogs.add(WaterfallDialog("main", [step1, step2])) Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 diff --git a/libraries/microsoft-agents-hosting-fastapi/readme.md b/libraries/microsoft-agents-hosting-fastapi/readme.md index 7de12b5d..0104ccee 100644 --- a/libraries/microsoft-agents-hosting-fastapi/readme.md +++ b/libraries/microsoft-agents-hosting-fastapi/readme.md @@ -9,6 +9,15 @@ This library provides FastAPI integration for Microsoft Agents, enabling you to Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -27,6 +36,15 @@ This library provides FastAPI integration for Microsoft Agents, enabling you to + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-hosting-msteams/readme.md b/libraries/microsoft-agents-hosting-msteams/readme.md index f4b2dfb0..d02a2020 100644 --- a/libraries/microsoft-agents-hosting-msteams/readme.md +++ b/libraries/microsoft-agents-hosting-msteams/readme.md @@ -18,6 +18,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** — a compre Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -36,6 +45,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** — a compre + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-hosting-slack/readme.md b/libraries/microsoft-agents-hosting-slack/readme.md index d514a393..7b225939 100644 --- a/libraries/microsoft-agents-hosting-slack/readme.md +++ b/libraries/microsoft-agents-hosting-slack/readme.md @@ -11,6 +11,15 @@ Integration library for building Slack agents using the Microsoft 365 Agents SDK Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 diff --git a/libraries/microsoft-agents-storage-blob/readme.md b/libraries/microsoft-agents-storage-blob/readme.md index 2dab87a8..f5721033 100644 --- a/libraries/microsoft-agents-storage-blob/readme.md +++ b/libraries/microsoft-agents-storage-blob/readme.md @@ -16,6 +16,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -34,6 +43,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/libraries/microsoft-agents-storage-cosmos/readme.md b/libraries/microsoft-agents-storage-cosmos/readme.md index 0e59223c..17081716 100644 --- a/libraries/microsoft-agents-storage-cosmos/readme.md +++ b/libraries/microsoft-agents-storage-cosmos/readme.md @@ -16,6 +16,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe Date Release Notes + + 1.2.0 + 2026-07-17 + + + 1.2.0 Release Notes + + + 1.1.0 2026-06-19 @@ -34,6 +43,15 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe + + 0.9.1 + 2026-05-04 + + + 0.9.1 Release Notes + + + 0.9.0 2026-04-15 diff --git a/tests/hosting_core/test_log_config.py b/tests/hosting_core/test_log_config.py new file mode 100644 index 00000000..669945c9 --- /dev/null +++ b/tests/hosting_core/test_log_config.py @@ -0,0 +1,146 @@ +import json +from unittest.mock import Mock + +import pytest + +from microsoft_agents.hosting.core import AgentAuthConfiguration +from microsoft_agents.hosting.core.authorization._log_config import ( + _log_config, + _redact_scopes, + _redact_str, + _redact_str_or_none, + _redact_url, + _redact_url_or_none, + _summarize_auth_configs, + _summarize_connections_map, +) + + +class TestRedactionUtils: + @pytest.mark.parametrize("value", ["", "short", "12345678"]) + def test_redact_str_without_peek(self, value): + assert _redact_str(value) == "..." + + def test_redact_str_with_peek_for_long_value(self): + assert _redact_str("client-id-secret", peek=True) == "cli..." + + @pytest.mark.parametrize("value", ["", "short", "12345678"]) + def test_redact_str_with_peek_for_short_values(self, value): + assert _redact_str(value, peek=True) == "..." + + def test_redact_str_or_none_returns_none_for_none(self): + assert _redact_str_or_none(None) is None + + def test_redact_str_or_none_redacts_value(self): + assert _redact_str_or_none("tenant-id-secret", peek=True) == "ten..." + + def test_redact_scopes_returns_none_for_none(self): + assert _redact_scopes(None) is None + + @pytest.mark.parametrize( + "scopes, expected", + [ + ([], "... [0 scope(s)]"), + (["scope1"], "... [1 scope(s)]"), + (["scope1", "scope2"], "... [2 scope(s)]"), + ], + ) + def test_redact_scopes_summarizes_count(self, scopes, expected): + assert _redact_scopes(scopes) == expected + + @pytest.mark.parametrize( + "url, expected", + [ + ( + "https://login.microsoftonline.com/tenant/oauth2/v2.0/token", + "https://login.microsoftonline.com/...", + ), + (" https://example.com/path?secret=value ", "https://example.com/..."), + ("", ""), + (" ", ""), + ], + ) + def test_redact_url(self, url, expected): + assert _redact_url(url) == expected + + def test_redact_url_or_none_returns_none_for_none(self): + assert _redact_url_or_none(None) is None + + def test_redact_url_or_none_redacts_value(self): + assert ( + _redact_url_or_none("https://example.com/path") == "https://example.com/..." + ) + + +class TestLogConfig: + def test_summarize_auth_configs_formats_connections_as_json_array(self): + summary = _summarize_auth_configs( + { + "SERVICE_CONNECTION": AgentAuthConfiguration( + client_id="client-id-secret", + tenant_id="tenant-id-secret", + client_secret="client-secret", + connection_name="configured-name", + authority="https://login.microsoftonline.com/tenant/oauth2/v2.0/token", + scopes=["scope1", "scope2"], + ) + } + ) + + parsed_summary = json.loads(summary) + + assert parsed_summary == [ + { + "CONNECTION": "SERVICE_CONNECTION", + "CONNECTION_NAME": "configured-name", + "CLIENTID": "cli...", + "TENANTID": "ten...", + "CLIENTSECRET": "...", + "AUTHORITY": "https://login.microsoftonline.com/...", + "SCOPES": "... [2 scope(s)]", + "ANONYMOUS_ALLOWED": "False", + } + ] + assert "client-secret" not in summary + assert "oauth2/v2.0/token" not in summary + + def test_summarize_connections_map_redacts_service_urls(self): + summary = _summarize_connections_map( + [ + { + "CONNECTION": "SERVICE_CONNECTION", + "AUDIENCE": "api://service", + "SERVICEURL": "https://service.example.com/path?secret=value", + }, + {"CONNECTION": "AGENTIC", "SERVICEURL": "*"}, + ] + ) + + parsed_summary = json.loads(summary) + + assert parsed_summary == [ + { + "CONNECTION": "SERVICE_CONNECTION", + "AUDIENCE": "api://service", + "SERVICEURL": "https://service.example.com/...", + }, + {"CONNECTION": "AGENTIC", "SERVICEURL": "*"}, + ] + assert "path?secret=value" not in summary + + def test_log_config_uses_clean_parameterized_format(self): + logger = Mock() + config_map = { + "SERVICE_CONNECTION": AgentAuthConfiguration(client_id="client-id-secret") + } + connections_map = [{"CONNECTION": "SERVICE_CONNECTION", "SERVICEURL": "*"}] + + _log_config(logger, config_map, connections_map) + + logger.info.assert_called_once() + message, connections_output, connections_map_output = logger.info.call_args.args + assert message == "\nConnections:\n%s\n\nConnections Map:\n%s" + assert json.loads(connections_output)[0]["CONNECTION"] == "SERVICE_CONNECTION" + assert json.loads(connections_map_output)[0]["CONNECTION"] == ( + "SERVICE_CONNECTION" + )