Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

appservice: support assign managed service identity to webapp/functionapp #4837

Merged
merged 6 commits into from
Nov 8, 2017
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
75 changes: 75 additions & 0 deletions src/azure-cli-core/azure/cli/core/commands/arm.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,3 +730,78 @@ def _find_property(instance, path):
for part in path:
instance = _update_instance(instance, part, path)
return instance


def assign_implict_identity(getter, setter, identity_role=None, identity_scope=None):
import time
from azure.mgmt.authorization import AuthorizationManagementClient
from azure.mgmt.authorization.models import RoleAssignmentProperties
from msrestazure.azure_exceptions import CloudError

# get
resource = getter()
if resource.identity:
logger.warning('Implict identity is already configured')
else:
resource = setter(resource)

# create role assignment:
if identity_scope:
principal_id = resource.identity.principal_id

identity_role_id = resolve_role_id(identity_role, identity_scope)
assignments_client = get_mgmt_service_client(AuthorizationManagementClient).role_assignments
properties = RoleAssignmentProperties(identity_role_id, principal_id)

logger.info("Creating an assignment with a role '%s' on the scope of '%s'", identity_role_id, identity_scope)
retry_times = 36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry 36 times! 1) Magic number? Any guidelines coming from identity team? 2) 36 times !?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's 3 minutes ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, recommended by the PAS team to workaround a known server replication delay :)

assignment_id = _gen_guid()
for l in range(0, retry_times):
try:
assignments_client.create(identity_scope, assignment_id, properties)
break
except CloudError as ex:
if 'role assignment already exists' in ex.message:
logger.info('Role assignment already exists')
break
elif l < retry_times and ' does not exist in the directory ' in ex.message:
time.sleep(5)
logger.warning('Retrying role assignment creation: %s/%s', l + 1,
retry_times)
continue
else:
raise
return resource


def resolve_role_id(role, scope):
import uuid
from azure.mgmt.authorization import AuthorizationManagementClient
client = get_mgmt_service_client(AuthorizationManagementClient).role_definitions

role_id = None
if re.match(r'/subscriptions/[^/]+/providers/Microsoft.Authorization/roleDefinitions/',
role, re.I):
role_id = role
else:
try:
uuid.UUID(role)
role_id = '/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/{}'.format(
client.config.subscription_id, role)
except ValueError:
pass
if not role_id: # retrieve role id
role_defs = list(client.list(scope, "roleName eq '{}'".format(role)))
if not role_defs:
raise CLIError("Role '{}' doesn't exist.".format(role))
elif len(role_defs) > 1:
ids = [r.id for r in role_defs]
err = "More than one role matches the given name '{}'. Please pick an id from '{}'"
raise CLIError(err.format(role, ids))
role_id = role_defs[0].id
return role_id


def _gen_guid():
import uuid
return uuid.uuid4()
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@
--facebook-oauth-scopes public_profile email
"""

helps['webapp assign-identity'] = """
type: command
short-summary: (PREVIEW) assign managed service identity to the webapp
examples:
- name: assign local identity and assign a reader role to the current resource group.
text: >
az webapp assign-identity -g MyResourceGroup -n MyUniqueApp --role reader --scope /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/MyResourceGroup
- name: disable the identity when there is need.
text: >
az webapp config appsettings set -g MyResourceGroup -n MyUniqueApp --settings WEBSITE_DISABLE_MSI=true
"""

helps['functionapp assign-identity'] = helps['webapp assign-identity'].replace('webapp', 'functionapp')

helps['webapp config'] = """
type: group
short-summary: Configure a web app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ def get_hostname_completion_list(prefix, action, parsed_args, **kwargs): # pyli
register_cli_argument(scope + ' create', 'deployment_zip', options_list=('--deployment-zip', '-z'), help='perform deployment using zip file')
register_cli_argument(scope + ' create', 'deployment_source_url', options_list=('--deployment-source-url', '-u'), help='Git repository URL to link with manual integration')
register_cli_argument(scope + ' create', 'deployment_source_branch', options_list=('--deployment-source-branch', '-b'), help='the branch to deploy')
register_cli_argument(scope + ' assign-identity', 'disable_msi', action='store_true', help='disable the identity')
register_cli_argument(scope + ' assign-identity', 'scope', help="The scope the managed identity has access to")
register_cli_argument(scope + ' assign-identity', 'role', help="Role name or id the managed identity will be assigned")

register_cli_argument('webapp config hostname', 'webapp_name', help="webapp name. You can configure the default using 'az configure --defaults web=<name>'", configured_default='web',
completer=get_resource_name_completion_list('Microsoft.Web/sites'), id_part='name')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def _polish_bad_errors(ex):
cli_command(__name__, 'webapp auth show', custom_path + 'get_auth_settings')
cli_command(__name__, 'webapp auth update', custom_path + 'update_auth_settings')

cli_command(__name__, 'webapp assign-identity', custom_path + 'assign_identity')

if not supported_api_version(PROFILE_TYPE, max_api='2017-03-09-profile'):
cli_command(__name__, 'appservice plan create', custom_path + 'create_app_service_plan', exception_handler=ex_handler_factory(creating_plan=True))
Expand Down Expand Up @@ -187,3 +188,4 @@ def _polish_bad_errors(ex):
cli_command(__name__, 'functionapp deployment list-publishing-profiles',
custom_path + 'list_publish_profiles')
cli_command(__name__, 'functionapp deployment user show', 'azure.mgmt.web.web_site_management_client#WebSiteManagementClient.get_publishing_user', cf_web_client, exception_handler=empty_on_404)
cli_command(__name__, 'functionapp assign-identity', custom_path + 'assign_identity')
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
SkuDescription, SslState, HostNameBinding, NameValuePair,
BackupRequest, DatabaseBackupSetting, BackupSchedule,
RestoreRequest, FrequencyUnit, Certificate, HostNameSslState,
RampUpRule, UnauthenticatedClientAction)
RampUpRule, UnauthenticatedClientAction, ManagedServiceIdentity)

from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azure.cli.core.commands import LongRunningOperation
Expand Down Expand Up @@ -131,6 +131,23 @@ def _list_app(app_types, resource_group_name=None):
return result


def assign_identity(resource_group_name, name, role='Contributor', scope=None, disable_msi=False):
client = web_client_factory()

def getter():
return _generic_site_operation(resource_group_name, name, 'get')

def setter(webapp):
webapp.identity = ManagedServiceIdentity(type='SystemAssigned')
poller = client.web_apps.create_or_update(resource_group_name, name, webapp)
return LongRunningOperation()(poller)

from azure.cli.core.commands.arm import assign_implict_identity
webapp = assign_implict_identity(getter, setter, role, scope)
update_app_settings(resource_group_name, name, ['WEBSITE_DISABLE_MSI={}'.format(disable_msi)])
return webapp.identity


def get_auth_settings(resource_group_name, name, slot=None):
return _generic_site_operation(resource_group_name, name, 'get_auth_settings', slot)

Expand Down
Loading