From dba6be00e67c405f05ab8ea3c35bce12d41f6f09 Mon Sep 17 00:00:00 2001 From: rohanpawar_microsoft Date: Wed, 29 Apr 2026 17:58:17 -0700 Subject: [PATCH 1/2] Support Custom ACR Scope for Disconnected Clouds (ALDO) --- .../cli/command_modules/acr/_docker_utils.py | 24 ++++++++++- .../tests/latest/test_acr_commands_mock.py | 42 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py b/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py index 9f648f8e6e6..0c60c15c664 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py +++ b/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py @@ -126,6 +126,28 @@ def _handle_challenge_phase(login_server, return token_params +def _resolve_acr_scope(cli_ctx): + """Determine the AAD resource (audience) to request a token for, for the ACR /oauth2/exchange endpoint. + + Resolution order: + 1. ``az config set acr.audience_resource=`` — operator override. If the value + starts with ``https://`` it is used verbatim, otherwise it is treated as a short + name and expanded to ``https://.azure.net``. + 2. The default public ACR audience ``https://containerregistry.azure.net``. + + This lets disconnected environments (e.g. Azure Local Disconnected Operations) pin + the audience their local IDP knows about, instead of relying on runtime fallback. + """ + configured = None + try: + configured = cli_ctx.config.get('acr', 'audience_resource', fallback=None) + except Exception: # pylint: disable=broad-except + configured = None + if configured: + return configured if configured.startswith('https://') else "https://{}.azure.net".format(configured) + return "https://{}.azure.net".format(ACR_AUDIENCE_RESOURCE_NAME) + + def _get_aad_token_after_challenge(cli_ctx, token_params, login_server, @@ -141,7 +163,7 @@ def _get_aad_token_after_challenge(cli_ctx, from azure.cli.core._profile import Profile profile = Profile(cli_ctx=cli_ctx) - scope = "https://{}.azure.net".format(ACR_AUDIENCE_RESOURCE_NAME) + scope = _resolve_acr_scope(cli_ctx) # this might be a cross tenant scenario, so pass subscription to get_raw_token creds, _, tenant = profile.get_raw_token(subscription=get_subscription_id(cli_ctx), diff --git a/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_commands_mock.py b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_commands_mock.py index fc4ce85ac56..95c8e3c34db 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_commands_mock.py +++ b/src/azure-cli/azure/cli/command_modules/acr/tests/latest/test_acr_commands_mock.py @@ -44,11 +44,13 @@ get_access_credentials, get_authorization_header, get_manifest_authorization_header, + _resolve_acr_scope, RepoAccessTokenPermission, HelmAccessTokenPermission, EMPTY_GUID ) from azure.cli.command_modules.acr._docker_utils import ResourceNotFound +from azure.cli.command_modules.acr._constants import ACR_AUDIENCE_RESOURCE_NAME from azure.cli.core.mock import DummyCli @@ -1446,3 +1448,43 @@ def _setup_cmd(self): mock_sku.premium.value = 'Premium' cmd.get_models.return_value = mock_sku return cmd + + +class ResolveAcrScopeTests(unittest.TestCase): + """Unit tests for _docker_utils._resolve_acr_scope. + + The helper resolves the AAD audience used for ACR's /oauth2/exchange call. + Operators can override the default via ``az config set acr.audience_resource=``. + """ + + @staticmethod + def _ctx(configured): + cli_ctx = mock.MagicMock() + cli_ctx.config.get.return_value = configured + return cli_ctx + + def test_default_when_unset(self): + self.assertEqual( + _resolve_acr_scope(self._ctx(None)), + "https://{}.azure.net".format(ACR_AUDIENCE_RESOURCE_NAME), + ) + + def test_short_name_is_expanded(self): + self.assertEqual( + _resolve_acr_scope(self._ctx("containerregistry")), + "https://containerregistry.azure.net", + ) + + def test_full_url_is_used_verbatim(self): + self.assertEqual( + _resolve_acr_scope(self._ctx("https://customregistry.example.com")), + "https://customregistry.example.com", + ) + + def test_config_exception_falls_back_to_default(self): + cli_ctx = mock.MagicMock() + cli_ctx.config.get.side_effect = RuntimeError("no config") + self.assertEqual( + _resolve_acr_scope(cli_ctx), + "https://{}.azure.net".format(ACR_AUDIENCE_RESOURCE_NAME), + ) From 2f1d59e921173f2125efa399be40ad419b3730d8 Mon Sep 17 00:00:00 2001 From: rohanpawar_microsoft Date: Wed, 29 Apr 2026 18:37:35 -0700 Subject: [PATCH 2/2] Remove whitespace --- src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py b/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py index 0c60c15c664..dd30846daa3 100644 --- a/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py +++ b/src/azure-cli/azure/cli/command_modules/acr/_docker_utils.py @@ -146,7 +146,7 @@ def _resolve_acr_scope(cli_ctx): if configured: return configured if configured.startswith('https://') else "https://{}.azure.net".format(configured) return "https://{}.azure.net".format(ACR_AUDIENCE_RESOURCE_NAME) - + def _get_aad_token_after_challenge(cli_ctx, token_params,