diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index d6488e0c6cb..44677cb37c5 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1537,10 +1537,12 @@ type: command short-summary: Import an SSL or App Service Certificate to a web app from Key Vault. examples: - - name: Import an SSL or App Service Certificate certificate to a web app from Key Vault. + - name: Import an SSL or App Service Certificate certificate to a web app from Key Vault. Note that all webapps in the webspace will also be able to use the certificate. text: az webapp config ssl import --resource-group MyResourceGroup --name MyWebapp --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName - - name: Import an SSL or App Service Certificate to a web app from Key Vault using resource id (typically if Key Vault is in another subscription). + - name: Import an SSL or App Service Certificate to a web app from Key Vault using resource id (typically if Key Vault is in another subscription). Note that all webapps in the webspace will also be able to use the certificate. text: az webapp config ssl import --resource-group MyResourceGroup --name MyWebapp --key-vault '/subscriptions/[sub id]/resourceGroups/[rg]/providers/Microsoft.KeyVault/vaults/[vault name]' --key-vault-certificate-name MyCertificateName + - name: Import an SSL or App Service Certificate certificate to a webspace from Key Vault. Note that all webapps in the webspace will also be able to use the certificate. + text: az webapp config ssl import --resource-group MyResourceGroup --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName """ helps['webapp config ssl create'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 01ed15af191..869a1fb2815 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -324,6 +324,7 @@ def load_arguments(self, _): with self.argument_context(scope + ' config ssl import') as c: c.argument('key_vault', help='The name or resource ID of the Key Vault') c.argument('key_vault_certificate_name', help='The name of the certificate in Key Vault') + c.argument('name', help='Name of the web app. This is used to set the location of the webspace for the certificate import. If not specified, the location of the resource group will be used. If you have apps in multiple regions/webspaces, you must specify the name of the app to set the location of the webspace for the certificate import.') with self.argument_context(scope + ' config ssl create') as c: c.argument('hostname', help='The custom domain name') c.argument('name', options_list=['--name', '-n'], help='Name of the web app.') 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 c34e8f1a937..2a91653263c 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -3975,14 +3975,22 @@ def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint): raise ResourceNotFoundError("Certificate for thumbprint '{}' not found".format(certificate_thumbprint)) -def import_ssl_cert(cmd, resource_group_name, name, key_vault, key_vault_certificate_name, certificate_name=None): +def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_name, name=None, certificate_name=None): Certificate = cmd.get_models('Certificate') client = web_client_factory(cmd.cli_ctx) - webapp = client.web_apps.get(resource_group_name, name) - if not webapp: - raise ResourceNotFoundError("'{}' app doesn't exist in resource group {}".format(name, resource_group_name)) - server_farm_id = webapp.server_farm_id - location = webapp.location + + # Webapp name is not required for this command, but the location of the webspace is required since the certificate + # is associated with the webspace, not the app. All apps and plans in the same webspace will share the same + # certificates. If the app is not provided, the location of the resource group is used. + if name: + webapp = client.web_apps.get(resource_group_name, name) + if not webapp: + raise ResourceNotFoundError("'{}' app doesn't exist in resource group {}".format(name, resource_group_name)) + location = webapp.location + else: + rg_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) + location = rg_client.resource_groups.get(resource_group_name).location + kv_id = None if not is_valid_resource_id(key_vault): kv_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_KEYVAULT) @@ -3997,9 +4005,9 @@ def import_ssl_cert(cmd, resource_group_name, name, key_vault, key_vault_certifi if kv_id is None: kv_msg = 'The Key Vault {0} was not found in the subscription in context. ' \ 'If your Key Vault is in a different subscription, please specify the full Resource ID: ' \ - '\naz .. ssl import -n {1} -g {2} --key-vault-certificate-name {3} ' \ + '\naz .. ssl import -g {1} --key-vault-certificate-name {2} ' \ '--key-vault /subscriptions/[sub id]/resourceGroups/[rg]/providers/Microsoft.KeyVault/' \ - 'vaults/{0}'.format(key_vault, name, resource_group_name, key_vault_certificate_name) + 'vaults/{0}'.format(key_vault, resource_group_name, key_vault_certificate_name) logger.warning(kv_msg) return @@ -4044,7 +4052,7 @@ def import_ssl_cert(cmd, resource_group_name, name, key_vault, key_vault_certifi logger.warning(lnk_msg) kv_cert_def = Certificate(location=location, key_vault_id=kv_id, password='', - key_vault_secret_name=kv_secret_name, server_farm_id=server_farm_id) + key_vault_secret_name=kv_secret_name) return client.certificates.create_or_update(name=cert_name, resource_group_name=resource_group_name, certificate_envelope=kv_cert_def) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl_import_no_app.yaml b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl_import_no_app.yaml new file mode 100644 index 00000000000..3024375b814 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl_import_no_app.yaml @@ -0,0 +1,105 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - keyvault set-policy + Connection: + - keep-alive + ParameterSetName: + - -g --name --spn --secret-permissions + User-Agent: + - python/3.12.9 (Windows-11-10.0.26100-SP0) AZURECLI/2.70.0 + method: GET + uri: https://graph.microsoft.com/v1.0/servicePrincipals?$filter=servicePrincipalNames%2Fany%28c%3Ac%20eq%20%27Microsoft.Azure.WebSites%27%29 + response: + body: + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#servicePrincipals","value":[{"id":"f8daea97-62e7-4026-becf-13c2ea98e8b4","deletedDateTime":null,"accountEnabled":true,"alternativeNames":[],"appDisplayName":"Microsoft + Azure App Service","appDescription":null,"appId":"abfa0a7c-a6b6-4736-8310-5855508787cd","applicationTemplateId":null,"appOwnerOrganizationId":"f8cdef31-a31e-4b4a-93e4-5f571e91255a","appRoleAssignmentRequired":false,"createdDateTime":null,"description":null,"disabledByMicrosoftStatus":null,"displayName":"Microsoft + Azure App Service","homepage":null,"loginUrl":null,"logoutUrl":null,"notes":null,"notificationEmailAddresses":[],"preferredSingleSignOnMode":null,"preferredTokenSigningKeyThumbprint":null,"replyUrls":["https://msftintch1.sso.azurewebsites.windows.net/","https://jinc.sso.azurewebsites.windows.net/","https://jinw.sso.azurewebsites.windows.net/","https://sec.sso.azurewebsites.windows.net/","https://qac.sso.azurewebsites.windows.net/","https://ses.sso.azurewebsites.windows.net/","https://plc.sso.azurewebsites.windows.net/","https://itn.sso.azurewebsites.windows.net/","https://ilc.sso.azurewebsites.windows.net/","https://esc.sso.azurewebsites.windows.net/","https://mxc.sso.azurewebsites.windows.net/","https://twn.sso.azurewebsites.windows.net/","https://twnw.sso.azurewebsites.windows.net/","https://hel.sso.azurewebsites.windows.net/","https://ate.sso.azurewebsites.windows.net/","https://kul.sso.azurewebsites.windows.net/","https://lo.sso.azurewebsites.windows.net/","https://cnn10.sso.azurewebsites.windows.net/","https://krs2.sso.azurewebsites.windows.net/","https://brne.sso.azurewebsites.windows.net/","https://clc.sso.azurewebsites.windows.net/","https://clnc.sso.azurewebsites.windows.net/","https://myw.sso.azurewebsites.windows.net/","https://nzn.sso.azurewebsites.windows.net/","https://bec.sso.azurewebsites.windows.net/","https://mys.sso.azurewebsites.windows.net/","https://insc.sso.azurewebsites.windows.net/","https://idc.sso.azurewebsites.windows.net/","https://flc.sso.azurewebsites.windows.net/","https://ilnw.sso.azurewebsites.windows.net/","https://dke.sso.azurewebsites.windows.net/","https://usw3.sso.azurewebsites.windows.net/","https://dxb.sso.azurewebsites.windows.net/","https://brse.sso.azurewebsites.windows.net/","https://chw.sso.azurewebsites.windows.net/","https://trydiagnosticsmesh.azure.com/","https://zrh.sso.azurewebsites.windows.net/","https://yt1.sso.azurewebsites.windows.net/","https://yq1.sso.azurewebsites.windows.net/","https://sy3.sso.azurewebsites.windows.net/","https://svg.sso.azurewebsites.windows.net/","https://sn1.sso.azurewebsites.windows.net/","https://sg1.sso.azurewebsites.windows.net/","https://se1.sso.azurewebsites.windows.net/","https://ps1.sso.azurewebsites.windows.net/","https://pn1.sso.azurewebsites.windows.net/","https://par.sso.azurewebsites.windows.net/","https://osl.sso.azurewebsites.windows.net/","https://os1.sso.azurewebsites.windows.net/","https://mwh.sso.azurewebsites.windows.net/","https://msftintsg1.sso.azurewebsites.windows.net/","https://msftintdm3.sso.azurewebsites.windows.net/","https://mrs.sso.azurewebsites.windows.net/","https://ml1.sso.azurewebsites.windows.net/","https://ma1.sso.azurewebsites.windows.net/","https://ln1.sso.azurewebsites.windows.net/","https://kw1.sso.azurewebsites.windows.net/","https://jnb21.sso.azurewebsites.windows.net/","https://hk1.sso.azurewebsites.windows.net/","https://fra.sso.azurewebsites.windows.net/","https://dm1.sso.azurewebsites.windows.net/","https://db3.sso.azurewebsites.windows.net/","https://cy4.sso.azurewebsites.windows.net/","https://cw1.sso.azurewebsites.windows.net/","https://cq1.sso.azurewebsites.windows.net/","https://cpt20.sso.azurewebsites.windows.net/","https://ch1.sso.azurewebsites.windows.net/","https://cbr21.sso.azurewebsites.windows.net/","https://cbr20.sso.azurewebsites.windows.net/","https://bn1.sso.azurewebsites.windows.net/","https://bm1.sso.azurewebsites.windows.net/","https://blu.sso.azurewebsites.windows.net/","https://ber.sso.azurewebsites.windows.net/","https://bay.sso.azurewebsites.windows.net/","https://auh.sso.azurewebsites.windows.net/","https://am2.sso.azurewebsites.windows.net/","https://euapdm1.sso.azurewebsites.windows.net/","https://euapbn1.sso.azurewebsites.windows.net/","https://msftinthk1.sso.azurewebsites.windows.net/","https://deploy-staging.azure.com","https://functions.azure.com","https://functions-staging.azure.com","https://functions-next.azure.com","https://functions-release.azure.com"],"servicePrincipalNames":["https://appservice.azure.com","Microsoft.Azure.WebSites","abfa0a7c-a6b6-4736-8310-5855508787cd"],"servicePrincipalType":"Application","signInAudience":"AzureADMultipleOrgs","tags":["disableAcceptingTenantedPassthroughTokens","disableRequestingTenantedPassthroughTokens","disableLegacyUserImpersonationResource","disableLegacyUserImpersonationClient"],"tokenEncryptionKeyId":null,"samlSingleSignOnSettings":null,"addIns":[],"appRoles":[],"info":{"logoUrl":null,"marketingUrl":null,"privacyStatementUrl":null,"supportUrl":null,"termsOfServiceUrl":null},"keyCredentials":[],"oauth2PermissionScopes":[{"adminConsentDescription":"Allow + the application to access all the APIs registered with App Service","adminConsentDisplayName":"Access + APIs registered with App Service","id":"e0ea806b-d128-49dc-ac08-2bf18f7874d8","isEnabled":true,"type":"User","userConsentDescription":"Allow + the application to access all the APIs registered with App Service","userConsentDisplayName":"Access + APIs registered with App Service","value":"user_impersonation"}],"passwordCredentials":[],"resourceSpecificApplicationPermissions":[],"verifiedPublisher":{"displayName":null,"verifiedPublisherId":null,"addedDateTime":null}}]}' + headers: + cache-control: + - no-cache + content-length: + - '5754' + content-type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + date: + - Wed, 05 Mar 2025 21:00:51 GMT + odata-version: + - '4.0' + request-id: + - 1c1c2816-f859-44c9-a943-29e634ead8a7 + strict-transport-security: + - max-age=31536000 + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-ms-ags-diagnostic: + - '{"ServerInfo":{"DataCenter":"East US 2","Slice":"E","Ring":"5","ScaleUnit":"002","RoleInstance":"BN2PEPF00003EA9"}}' + x-ms-resource-unit: + - '1' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - keyvault set-policy + Connection: + - keep-alive + ParameterSetName: + - -g --name --spn --secret-permissions + User-Agent: + - AZURECLI/2.70.0 azsdk-python-core/1.31.0 Python/3.12.9 (Windows-11-10.0.26100-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.KeyVault/vaults/kv-ssl-test000002?api-version=2023-02-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.KeyVault/vaults/kv-ssl-test000002","name":"kv-ssl-test000002","type":"Microsoft.KeyVault/vaults","location":"westeurope","tags":{},"systemData":{"createdBy":"jordanselig@microsoft.com","createdByType":"User","createdAt":"2025-03-05T21:00:15.656Z","lastModifiedBy":"jordanselig@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2025-03-05T21:00:15.656Z"},"properties":{"sku":{"family":"A","name":"standard"},"tenantId":"72f988bf-86f1-41af-91ab-2d7cd011db47","accessPolicies":[],"enabledForDeployment":false,"enableSoftDelete":true,"softDeleteRetentionInDays":7,"enableRbacAuthorization":true,"vaultUri":"https://kv-ssl-test000002.vault.azure.net/","provisioningState":"Succeeded","publicNetworkAccess":"Enabled"}}' + headers: + cache-control: + - no-cache + content-length: + - '834' + content-type: + - application/json; charset=utf-8 + date: + - Wed, 05 Mar 2025 21:00:52 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + x-aspnet-version: + - 4.0.30319 + x-cache: + - CONFIG_NOCACHE + x-content-type-options: + - nosniff + x-ms-keyvault-service-version: + - 1.5.1491.0 + x-ms-ratelimit-remaining-subscription-global-reads: + - '16499' + x-msedge-ref: + - 'Ref A: A373C60BBBF64BA3B5A74C6EE6A7B06B Ref B: BL2AA2030101019 Ref C: 2025-03-05T21:00:52Z' + status: + code: 200 + message: OK +version: 1 diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py index f521b78d1a9..4f8e297263d 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py @@ -1857,6 +1857,26 @@ def test_webapp_ssl_import(self, resource_group, key_vault): webapp_name), cert_thumbprint) ]) + @unittest.skip("Flaky Test") + @ResourceGroupPreparer(location=WINDOWS_ASP_LOCATION_WEBAPP) + @KeyVaultPreparer(location=WINDOWS_ASP_LOCATION_WEBAPP, name_prefix='kv-ssl-test', name_len=20) + def test_webapp_ssl_import_no_app(self, resource_group, key_vault): + # Cert Generated using + # https://learn.microsoft.com/azure/app-service-web/web-sites-configure-ssl-certificate#bkmk_ssopenssl + pfx_file = os.path.join(TEST_DIR, 'server.pfx') + cert_password = 'test' + cert_thumbprint = '9E9735C45C792B03B3FFCCA614852B32EE71AD6B' + cert_name = 'test-cert' + self.cmd('keyvault set-policy -g {} --name {} --spn {} --secret-permissions get'.format( + resource_group, key_vault, 'Microsoft.Azure.WebSites')) + self.cmd('keyvault certificate import --name {} --vault-name {} --file "{}" --password {}'.format( + cert_name, key_vault, pfx_file, cert_password)) + + self.cmd('webapp config ssl import --resource-group {} --key-vault {} --key-vault-certificate-name {} --certificate-name {}'.format(resource_group, key_vault, cert_name, "test123"), checks=[ + JMESPathCheck('thumbprint', cert_thumbprint), + JMESPathCheck('test123') + ]) + @unittest.skip("Flaky Test") @ResourceGroupPreparer(parameter_name='kv_resource_group', location=WINDOWS_ASP_LOCATION_WEBAPP) @ResourceGroupPreparer(location=WINDOWS_ASP_LOCATION_WEBAPP)