From 9d72a5f39cd12369d6fca64a2cd44b21d27f10e9 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Mon, 6 Jul 2026 11:24:34 -0400 Subject: [PATCH 1/2] {App Service} `az webapp config ssl import`: Fix AttributeError for App Service Certificates with azure-mgmt-web 11.0.0 The azure-mgmt-web 11.0.0 upgrade switched WebSiteManagementClient to a single-api (2025-05-01) client that no longer exposes the 'app_service_certificate_orders' operation group, which lives under the Microsoft.CertificateRegistration provider. This caused 'az webapp config ssl import' to fail with "'WebSiteManagementClient' object has no attribute 'app_service_certificate_orders'". Resolve the App Service Certificate Key Vault secret name via a direct ARM REST call to Microsoft.CertificateRegistration/certificateOrders instead. Fixes #33542 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 45 ++++++---- .../tests/latest/test_import_ssl_cert.py | 88 +++++++++++++++++++ 2 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_import_ssl_cert.py diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 31d333d7d28..7d11b6b3ef3 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -6923,6 +6923,29 @@ def _validate_flex_ssl_params(is_flex, load_to_code, enable_using_msi, webapp, r 'Ensure the app\'s managed identity has the permission on the Key Vault.') +def _get_app_service_certificate_kv_secret_name(cmd, subscription_id, key_vault_certificate_name): + # App Service Certificate orders are exposed by the Microsoft.CertificateRegistration resource provider, + # which the WebSiteManagementClient SDK no longer surfaces, so query the ARM REST API directly. + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + request_url = "{}/subscriptions/{}/providers/Microsoft.CertificateRegistration/certificateOrders" \ + "?api-version={}".format(management_hostname.strip('/'), subscription_id, VERSION_2022_09_01) + try: + while request_url: + response = send_raw_request(cmd.cli_ctx, method='get', url=request_url).json() + for asc in response.get('value', []): + if asc.get('name') == key_vault_certificate_name: + certificates = asc.get('properties', {}).get('certificates') or {} + cert = certificates.get(key_vault_certificate_name) + if cert: + return cert.get('keyVaultSecretName') + request_url = response.get('nextLink') + except Exception as ex: # pylint: disable=broad-except + logger.warning("Unable to check App Service Certificate orders (%s). If '%s' is an App Service " + "Certificate, the import may not resolve the correct Key Vault secret name.", + str(ex), key_vault_certificate_name) + return None + + def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_name, name=None, certificate_name=None, load_to_code=None, enable_using_msi=None): Certificate = cmd.get_models('Certificate') @@ -6966,27 +6989,13 @@ def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_n kv_subscription = kv_id_parts['subscription'] # If in the public cloud, check if certificate is an app service certificate, in the same or a diferent - # subscription + # subscription. App Service Certificate orders belong to the Microsoft.CertificateRegistration provider, + # which is no longer exposed by the WebSiteManagementClient SDK, so the ARM REST API is queried directly. kv_secret_name = None cloud_type = cmd.cli_ctx.cloud.name - from azure.cli.core.commands.client_factory import get_subscription_id - subscription_id = get_subscription_id(cmd.cli_ctx) if cloud_type.lower() == PUBLIC_CLOUD.lower(): - if kv_subscription.lower() != subscription_id.lower(): - cert_orders_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_APPSERVICE, - subscription_id=kv_subscription) - else: - cert_orders_client = client - - if hasattr(cert_orders_client, 'app_service_certificate_orders'): - ascs = cert_orders_client.app_service_certificate_orders.list(api_version=VERSION_2022_09_01) - for asc in ascs: - if asc.name == key_vault_certificate_name: - kv_secret_name = asc.certificates[key_vault_certificate_name].key_vault_secret_name - else: - logger.warning("Unable to check App Service Certificate orders. " - "If '%s' is an App Service Certificate, the import may not resolve " - "the correct Key Vault secret name.", key_vault_certificate_name) + kv_secret_name = _get_app_service_certificate_kv_secret_name(cmd, kv_subscription, + key_vault_certificate_name) # if kv_secret_name is not populated, it is not an appservice certificate, proceed for KV certificates kv_secret_name = kv_secret_name or key_vault_certificate_name diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_import_ssl_cert.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_import_ssl_cert.py new file mode 100644 index 00000000000..7ca26548324 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_import_ssl_cert.py @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +""" +Unit tests for App Service Certificate order resolution used by `az webapp config ssl import`. + +These validate that the Key Vault secret name is resolved via the ARM REST API for the +Microsoft.CertificateRegistration provider (which the WebSiteManagementClient SDK no longer +exposes), without requiring Azure connectivity. +""" + +import unittest +from unittest.mock import MagicMock, patch + +from azure.cli.command_modules.appservice.custom import _get_app_service_certificate_kv_secret_name + +_CUSTOM_MOD = "azure.cli.command_modules.appservice.custom" + + +def _make_cmd(): + cmd = MagicMock() + cmd.cli_ctx.cloud.endpoints.resource_manager = "https://management.azure.com" + return cmd + + +def _make_response(payload): + response = MagicMock() + response.json.return_value = payload + return response + + +class TestAppServiceCertificateSecretResolution(unittest.TestCase): + def test_resolves_secret_name_for_matching_order(self): + cmd = _make_cmd() + payload = { + "value": [ + { + "name": "my-cert", + "properties": { + "certificates": { + "my-cert": {"keyVaultSecretName": "the-secret-name"} + } + }, + } + ] + } + with patch(f"{_CUSTOM_MOD}.send_raw_request", return_value=_make_response(payload)) as mock_req: + result = _get_app_service_certificate_kv_secret_name(cmd, "sub-id", "my-cert") + + self.assertEqual(result, "the-secret-name") + called_url = mock_req.call_args.kwargs.get("url") or mock_req.call_args.args[-1] + self.assertIn("/subscriptions/sub-id/providers/Microsoft.CertificateRegistration/certificateOrders", called_url) + + def test_returns_none_when_no_matching_order(self): + cmd = _make_cmd() + payload = {"value": [{"name": "other-cert", "properties": {"certificates": {}}}]} + with patch(f"{_CUSTOM_MOD}.send_raw_request", return_value=_make_response(payload)): + result = _get_app_service_certificate_kv_secret_name(cmd, "sub-id", "my-cert") + + self.assertIsNone(result) + + def test_follows_pagination_next_link(self): + cmd = _make_cmd() + page1 = {"value": [{"name": "other", "properties": {"certificates": {}}}], "nextLink": "https://next"} + page2 = { + "value": [ + {"name": "my-cert", "properties": {"certificates": {"my-cert": {"keyVaultSecretName": "secret2"}}}} + ] + } + with patch(f"{_CUSTOM_MOD}.send_raw_request", + side_effect=[_make_response(page1), _make_response(page2)]) as mock_req: + result = _get_app_service_certificate_kv_secret_name(cmd, "sub-id", "my-cert") + + self.assertEqual(result, "secret2") + self.assertEqual(mock_req.call_count, 2) + + def test_swallows_request_errors_and_returns_none(self): + cmd = _make_cmd() + with patch(f"{_CUSTOM_MOD}.send_raw_request", side_effect=Exception("boom")): + result = _get_app_service_certificate_kv_secret_name(cmd, "sub-id", "my-cert") + + self.assertIsNone(result) + + +if __name__ == "__main__": + unittest.main() From a305ded22933e47335d58a5e97c3e9af2fcbf12b Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Mon, 6 Jul 2026 11:45:33 -0400 Subject: [PATCH 2/2] Fix typo in import_ssl_cert comment (diferent -> different) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/azure-cli/azure/cli/command_modules/appservice/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 7d11b6b3ef3..2d75bc53f31 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -6988,7 +6988,7 @@ def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_n kv_resource_group_name = kv_id_parts['resource_group'] kv_subscription = kv_id_parts['subscription'] - # If in the public cloud, check if certificate is an app service certificate, in the same or a diferent + # If in the public cloud, check if certificate is an app service certificate in the same or a different # subscription. App Service Certificate orders belong to the Microsoft.CertificateRegistration provider, # which is no longer exposed by the WebSiteManagementClient SDK, so the ARM REST API is queried directly. kv_secret_name = None