-
Notifications
You must be signed in to change notification settings - Fork 86
Startup configuration logging of connections and connections map #474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Rodrigo Brandão (rodrigobr-msft)
merged 9 commits into
main
from
users/robrandao/startup-logging
Jul 21, 2026
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
56d2405
Auth configuration logging with redaction for sensitive values
rodrigobr-msft 104719e
Merge branch 'main' into users/robrandao/startup-logging
rodrigobr-msft 545e579
Merge branch 'main' into users/robrandao/startup-logging
rodrigobr-msft 21fd325
Moving startup logging to hosting-core
rodrigobr-msft acb0ceb
Merge branch 'users/robrandao/startup-logging' of https://github.com/…
rodrigobr-msft 2ae9013
Formatting
rodrigobr-msft e61f1d4
Adjusting startup log newlines
rodrigobr-msft d3c1e24
Adjustment to formatting
rodrigobr-msft 6fa0ab6
Merge branch 'main' into users/robrandao/startup-logging
rodrigobr-msft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
.../microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/_log_config.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 "..." | ||
|
rodrigobr-msft marked this conversation as resolved.
|
||
|
|
||
|
|
||
| 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), | ||
|
Comment on lines
+120
to
+122
|
||
| ) | ||
| ) | ||
| 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 | ||
|
rodrigobr-msft marked this conversation as resolved.
|
||
|
|
||
| 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, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.