Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/azure-cli/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Release History

* Fix issue #11217: webapp: az webapp config ssl upload should support slot parameter
* Fix issue #10965: Error: Name cannot be empty. Allow remove by ip_address and subnet
* Add support for importing certificates from Key Vault `az webapp config ssl import`

**Compute**

Expand Down
16 changes: 16 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,14 @@
crafted: true
"""

helps['functionapp config ssl import'] = """
type: command
short-summary: Import an SSL certificate to a function app from Key Vault.
examples:
- name: Import an SSL certificate to a function app from Key Vault.
text: az functionapp config ssl import --resource-group MyResourceGroup --name MyFunctionApp --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName
"""

helps['functionapp cors'] = """
type: group
short-summary: Manage Cross-Origin Resource Sharing (CORS)
Expand Down Expand Up @@ -1164,6 +1172,14 @@
crafted: true
"""

helps['webapp config ssl import'] = """
type: command
short-summary: Import an SSL certificate to a web app from Key Vault.
examples:
- name: Import an SSL certificate to a web app from Key Vault.
text: az webapp config ssl import --resource-group MyResourceGroup --name MyWebapp --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName
"""

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ def load_arguments(self, _):
with self.argument_context(scope + ' config appsettings') as c:
c.argument('settings', nargs='+', help="space-separated app settings in a format of <name>=<value>")
c.argument('setting_names', nargs='+', help="space-separated app setting names")

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 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def load_command_table(self, _):
g.custom_command('bind', 'bind_ssl_cert', exception_handler=ex_handler_factory(), validator=validate_app_or_slot_exists_in_rg)
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)

with self.command_group('webapp config backup') as g:
g.custom_command('list', 'list_backups')
Expand Down Expand Up @@ -283,6 +284,7 @@ def load_command_table(self, _):
g.custom_command('bind', 'bind_ssl_cert', exception_handler=ex_handler_factory())
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)

with self.command_group('functionapp deployment source') as g:
g.custom_command('config-local-git', 'enable_local_git')
Expand Down
63 changes: 63 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,54 @@ def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint):
raise CLIError("Certificate for thumbprint '{}' not found".format(certificate_thumbprint))


def import_ssl_cert(cmd, resource_group_name, name, key_vault, key_vault_certificate_name):
client = web_client_factory(cmd.cli_ctx)
webapp = client.web_apps.get(resource_group_name, name)
if not webapp:
raise CLIError("'{}' app doesn't exist in resource group {}".format(name, resource_group_name))
server_farm_id = webapp.server_farm_id
location = webapp.location
kv_id = _format_key_vault_id(cmd.cli_ctx, key_vault, resource_group_name)
kv_id_parts = parse_resource_id(kv_id)
kv_name = kv_id_parts['name']
kv_resource_group_name = kv_id_parts['resource_group']
cert_name = '{}-{}-{}'.format(resource_group_name, kv_name, key_vault_certificate_name)
lnk = 'https://azure.github.io/AppService/2016/05/24/Deploying-Azure-Web-App-Certificate-through-Key-Vault.html'
lnk_msg = 'Find more details here: {}'.format(lnk)
if not _check_service_principal_permissions(cmd, kv_resource_group_name, kv_name):
logger.warning('Unable to verify Key Vault permissions.')
logger.warning('You may need to grant Microsoft.Azure.WebSites service principal the Secret:Get permission')
logger.warning(lnk_msg)

kv_cert_def = Certificate(location=location, key_vault_id=kv_id, password='',
key_vault_secret_name=key_vault_certificate_name, server_farm_id=server_farm_id)

return client.certificates.create_or_update(name=cert_name, resource_group_name=resource_group_name,
certificate_envelope=kv_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
from azure.graphrbac.models import GraphErrorException
kv_client = keyvault_client_vaults_factory(cmd.cli_ctx, None)
vault = kv_client.get(resource_group_name=resource_group_name, vault_name=key_vault_name)
# Check for Microsoft.Azure.WebSites app registration
AZURE_PUBLIC_WEBSITES_APP_ID = 'abfa0a7c-a6b6-4736-8310-5855508787cd'
AZURE_GOV_WEBSITES_APP_ID = '6a02c803-dafd-4136-b4c3-5a6f318b4714'
graph_sp_client = _graph_client_factory(cmd.cli_ctx).service_principals
for policy in vault.properties.access_policies:
try:
sp = graph_sp_client.get(policy.object_id)
if sp.app_id == AZURE_PUBLIC_WEBSITES_APP_ID or sp.app_id == AZURE_GOV_WEBSITES_APP_ID:
for perm in policy.permissions.secrets:
if perm == "Get":
return True
except GraphErrorException:
pass # Lookup will fail for non service principals (users, groups, etc.)
return False


def _update_host_name_ssl_state(cli_ctx, resource_group_name, webapp_name, webapp,
host_name, ssl_state, thumbprint, slot=None):
updated_webapp = Site(host_name_ssl_states=[HostNameSslState(name=host_name,
Expand Down Expand Up @@ -3251,3 +3299,18 @@ def _validate_asp_sku(app_service_environment, sku):
if app_service_environment:
raise CLIError("Only pricing tier 'Isolated' is allowed in this app service plan. Use this link to "
"learn more: https://docs.microsoft.com/en-us/azure/app-service/overview-hosting-plans")


def _format_key_vault_id(cli_ctx, key_vault, resource_group_name):
key_vault_is_id = is_valid_resource_id(key_vault)
if key_vault_is_id:
return key_vault

from msrestazure.tools import resource_id
from azure.cli.core.commands.client_factory import get_subscription_id
return resource_id(
subscription=get_subscription_id(cli_ctx),
resource_group=resource_group_name,
namespace='Microsoft.KeyVault',
type='vaults',
name=key_vault)
Loading