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 debc7083bdf..ebe307549f4 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -328,6 +328,14 @@ text: az functionapp config ssl import --resource-group MyResourceGroup --name MyFunctionApp --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName """ +helps['functionapp config ssl create'] = """ +type: command +short-summary: Create a Managed Certificate for a hostname in a function app. +examples: + - name: Create a Managed Certificate for $fqdn. + text: az functionapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname $fqdn +""" + helps['functionapp cors'] = """ type: group short-summary: Manage Cross-Origin Resource Sharing (CORS) @@ -1180,6 +1188,14 @@ text: az webapp config ssl import --resource-group MyResourceGroup --name MyWebapp --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName """ +helps['webapp config ssl create'] = """ +type: command +short-summary: Create a Managed Certificate for a hostname in a webapp app. +examples: + - name: Create a Managed Certificate for $fqdn. + text: az webapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname $fqdn +""" + helps['webapp config storage-account'] = """ type: group short-summary: Manage a web app's Azure storage account configurations. (Linux Web Apps and Windows Containers Web Apps Only) 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 0609bee60cf..e5824280a6a 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -177,6 +177,8 @@ 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') + with self.argument_context(scope + ' config ssl create') as c: + c.argument('hostname', help='The custom domain name') with self.argument_context(scope + ' config hostname') as c: c.argument('hostname', completer=get_hostname_completion_list, help="hostname assigned to the site, such as custom domains", id_part='child_name_1') with self.argument_context(scope + ' deployment user') as c: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/commands.py b/src/azure-cli/azure/cli/command_modules/appservice/commands.py index ac721fd955f..0844a473ca0 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -144,6 +144,7 @@ def load_command_table(self, _): g.custom_command('unbind', 'unbind_ssl_cert', validator=validate_app_or_slot_exists_in_rg) g.custom_command('delete', 'delete_ssl_cert', exception_handler=ex_handler_factory()) g.custom_command('import', 'import_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True) + g.custom_command('create', 'create_managed_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True) with self.command_group('webapp config backup') as g: g.custom_command('list', 'list_backups') @@ -285,6 +286,7 @@ def load_command_table(self, _): g.custom_command('unbind', 'unbind_ssl_cert') g.custom_command('delete', 'delete_ssl_cert') g.custom_command('import', 'import_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True) + g.custom_command('create', 'create_managed_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True) with self.command_group('functionapp deployment source') as g: g.custom_command('config-local-git', 'enable_local_git') 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 1a84304164a..c43f9e6daad 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -2054,6 +2054,35 @@ def import_ssl_cert(cmd, resource_group_name, name, key_vault, key_vault_certifi certificate_envelope=kv_cert_def) +def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None): + Certificate = cmd.get_models('Certificate') + hostname = hostname.lower() + client = web_client_factory(cmd.cli_ctx) + webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) + if not webapp: + slot_text = "Deployment slot {} in ".format(slot) if slot else '' + raise CLIError("{0}app {1} doesn't exist in resource group {2}".format(slot_text, name, resource_group_name)) + + parsed_plan_id = parse_resource_id(webapp.server_farm_id) + plan_info = client.app_service_plans.get(parsed_plan_id['resource_group'], parsed_plan_id['name']) + if plan_info.sku.tier.upper() == 'FREE' or plan_info.sku.tier.upper() == 'SHARED': + raise CLIError('Managed Certificate is not supported on Free and Shared tier.') + + if not _verify_hostname_binding(cmd, resource_group_name, name, hostname, slot): + slot_text = " --slot {}".format(slot) if slot else "" + raise CLIError("Hostname (custom domain) '{0}' is not registered with {1}. " + "Use 'az webapp config hostname add --resource-group {2} " + "--webapp-name {1}{3} --hostname {0}' " + "to register the hostname.".format(hostname, name, resource_group_name, slot_text)) + + server_farm_id = webapp.server_farm_id + location = webapp.location + easy_cert_def = Certificate(location=location, canonical_name=hostname, + server_farm_id=server_farm_id, password='') + return client.certificates.create_or_update(name=hostname, resource_group_name=resource_group_name, + certificate_envelope=easy_cert_def) + + def _check_service_principal_permissions(cmd, resource_group_name, key_vault_name): from azure.cli.command_modules.keyvault._client_factory import keyvault_client_vaults_factory from azure.cli.command_modules.role._client_factory import _graph_client_factory @@ -3350,3 +3379,15 @@ def _format_key_vault_id(cli_ctx, key_vault, resource_group_name): namespace='Microsoft.KeyVault', type='vaults', name=key_vault) + + +def _verify_hostname_binding(cmd, resource_group_name, name, hostname, slot=None): + hostname_bindings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, + 'list_host_name_bindings', slot) + verified_hostname_found = False + for hostname_binding in hostname_bindings: + binding_name = hostname_binding.name.split('/')[-1] + if binding_name.lower() == hostname and hostname_binding.host_name_type == 'Verified': + verified_hostname_found = True + + return verified_hostname_found diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 393cbc55ef6..c7bb56b26e3 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -26,7 +26,8 @@ validate_container_app_create_options, restore_deleted_webapp, list_snapshots, - restore_snapshot) + restore_snapshot, + create_managed_ssl_cert) # pylint: disable=line-too-long from vsts_cd_manager.continuous_delivery_manager import ContinuousDeliveryResult @@ -403,6 +404,38 @@ def test_valid_linux_create_options(self): self.assertFalse(validate_container_app_create_options(None, None, test_multi_container_config, None)) self.assertFalse(validate_container_app_create_options(None, None, None, None)) + @mock.patch('azure.cli.command_modules.appservice.custom._verify_hostname_binding', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True) + def test_create_managed_ssl_cert(self, generic_site_op_mock, client_factory_mock, verify_binding_mock): + webapp_name = 'someWebAppName' + rg_name = 'someRgName' + farm_id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.Web/serverfarms/farm1' + host_name = 'www.contoso.com' + + client = mock.MagicMock() + client_factory_mock.return_value = client + cmd_mock = _get_test_cmd() + cli_ctx_mock = mock.MagicMock() + cli_ctx_mock.data = {'subscription_id': 'sub1'} + cmd_mock.cli_ctx = cli_ctx_mock + Site, Certificate = cmd_mock.get_models('Site', 'Certificate') + site = Site(name=webapp_name, location='westeurope') + site.server_farm_id = farm_id + generic_site_op_mock.return_value = site + + verify_binding_mock.return_value = False + with self.assertRaises(CLIError): + create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None) + + verify_binding_mock.return_value = True + create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None) + + cert_def = Certificate(location='westeurope', canonical_name=host_name, + server_farm_id=farm_id, password='') + client.certificates.create_or_update.assert_called_once_with(name=host_name, resource_group_name=rg_name, + certificate_envelope=cert_def) + class FakedResponse(object): # pylint: disable=too-few-public-methods def __init__(self, status_code):