Skip to content

Commit

Permalink
Container Instance: Role Assignment for System Assigned MSI (#7577)
Browse files Browse the repository at this point in the history
* adding deployment based for resources

* changing to sdk calls for role assignment

* Fixing naming problem causing authorization client to fail

* Fixing formatting errors

* fixing differnt return statemnet errors

* adding tests for the --scope parameter

* fixing conflicts in recordings

* fixing conflicts and rerecording tests

* removing whitespace

* making non predictble test live only

* Fixing blank lines

* allow role assignment for --no-wait aswell

* removed unused imports
  • Loading branch information
samkreter authored and williexu committed Oct 18, 2018
1 parent e0b46a9 commit 8c0e296
Show file tree
Hide file tree
Showing 14 changed files with 808 additions and 266 deletions.
1 change: 1 addition & 0 deletions src/command_modules/azure-cli-container/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Release History
0.3.6
+++++
* Add '--assign-identity' for adding a MSI identity to a container group
* Add '--scope' to create a role assignment for the system assigned MSI identity
* Show warning when creating a container group with an image without a long running process
* Fix table output issues for 'list' and 'show' commands

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ def cf_resource(cli_ctx):
return get_mgmt_service_client(cli_ctx, ResourceManagementClient)


def get_auth_management_client(cli_ctx, scope=None, **_):
import re
from azure.cli.core.profiles import ResourceType
from azure.cli.core.commands.client_factory import get_mgmt_service_client

subscription_id = None
if scope:
matched = re.match('/subscriptions/(?P<subscription>[^/]*)/', scope)
if matched:
subscription_id = matched.groupdict()['subscription']
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION, subscription_id=subscription_id)


def cf_network(cli_ctx):
from azure.mgmt.network import NetworkManagementClient
from azure.cli.core.commands.client_factory import get_mgmt_service_client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
text: az container create -g MyResourceGroup --name myapp --log-analytics-workspace myworkspace
- name: Create a container group with a system assigned identity.
text: az container create -g MyResourceGroup --name myapp --image myimage:latest --assign-identity
- name: Create a container group with a system assigned identity. The group will have a 'Contributor' role with access to a storage account.
text: az container create -g MyResourceGroup --name myapp --image myimage:latest --assign-identity --scope /subscriptions/99999999-1bf0-4dda-aec3-cb9272f09590/MyResourceGroup/myRG/providers/Microsoft.Storage/storageAccounts/storage1
- name: Create a container group with a user assigned identity.
text: az container create -g MyResourceGroup --name myapp --image myimage:latest --assign-identity /subscriptions/mySubscrpitionId/resourcegroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myID
- name: Create a container group with both system and user assigned identity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from azure.cli.core.commands.validators import get_default_location_from_resource_group
from azure.mgmt.containerinstance.models import (
ContainerGroupRestartPolicy, OperatingSystemTypes, ContainerNetworkProtocol)
from ._validators import (validate_volume_mount_path, validate_secrets, validate_subnet,
from ._validators import (validate_volume_mount_path, validate_secrets, validate_subnet, validate_msi,
validate_gitrepo_directory, validate_network_profile, validate_image)

# pylint: disable=line-too-long
Expand Down Expand Up @@ -78,7 +78,12 @@ def load_arguments(self, _):
c.argument('secrets', secrets_type)
c.argument('secrets_mount_path', validator=validate_volume_mount_path, help="The path within the container where the secrets volume should be mounted. Must not contain colon ':'.")
c.argument('file', options_list=['--file', '-f'], help="The path to the input file.")
c.argument('assign_identity', nargs='*', arg_group='Managed Service Identity', help="Space-separated list of assigned identities. Assigned identities are either user assigned identities (resource IDs) and / or the system assigned identity ('[system]'). See examples for more info.")

with self.argument_context('container create', arg_group='Managed Service Identity') as c:
c.argument('assign_identity', nargs='*', validator=validate_msi, help="Space-separated list of assigned identities. Assigned identities are either user assigned identities (resource IDs) and / or the system assigned identity ('[system]'). See examples for more info.")
c.argument('identity_scope', options_list=['--scope'], help="Scope that the system assigned identity can access")
c.argument('identity_role', options_list=['--role'], help="Role name or id the system assigned identity will have")
c.ignore('identity_role_id')

with self.argument_context('container create', arg_group='Network') as c:
c.argument('network_profile', network_profile_type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,54 @@ def validate_image(ns):
ns.image)


def validate_msi(cmd, namespace):
MSI_LOCAL_ID = '[system]'
if namespace.assign_identity is not None:
identities = namespace.assign_identity or []
if not namespace.identity_scope and getattr(namespace.identity_role, 'is_default', None) is None:
raise CLIError("usage error: '--role {}' is not applicable as the '--scope' is not provided".format(
namespace.identity_role))

if namespace.identity_scope:
if identities and MSI_LOCAL_ID not in identities:
raise CLIError("usage error: '--scope'/'--role' is only applicable when assign system identity")
# keep 'identity_role' for output as logical name is more readable
setattr(namespace, 'identity_role_id', _resolve_role_id(cmd.cli_ctx, namespace.identity_role,
namespace.identity_scope))
elif namespace.identity_scope or getattr(namespace.identity_role, 'is_default', None) is None:
raise CLIError('usage error: --assign-identity [--scope SCOPE] [--role ROLE]')


def _resolve_role_id(cli_ctx, role, scope):
import re
import uuid
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azure.cli.core.profiles import ResourceType

client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION).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 validate_subnet(ns):
from msrestazure.tools import is_valid_resource_id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
GitRepoVolume, LogAnalytics, ContainerGroupDiagnostics, ContainerGroupNetworkProfile,
ContainerGroupIpAddressType, ResourceIdentityType, ContainerGroupIdentity)
from azure.cli.core.util import sdk_no_wait
from ._client_factory import cf_container_groups, cf_container, cf_log_analytics, cf_resource, cf_network

from ._client_factory import (cf_container_groups, cf_container, cf_log_analytics, cf_resource,
cf_network, get_auth_management_client)

logger = get_logger(__name__)
WINDOWS_NAME = 'Windows'
Expand Down Expand Up @@ -105,6 +105,9 @@ def create_container(cmd,
secrets_mount_path=None,
file=None,
assign_identity=None,
identity_scope=None,
identity_role='Contributor',
identity_role_id=None,
no_wait=False):
"""Create a container group. """
if file:
Expand Down Expand Up @@ -215,7 +218,15 @@ def create_container(cmd,
tags=tags)

container_group_client = cf_container_groups(cmd.cli_ctx)
return sdk_no_wait(no_wait, container_group_client.create_or_update, resource_group_name, name, cgroup)

lro = sdk_no_wait(no_wait, container_group_client.create_or_update, resource_group_name,
name, cgroup)

if assign_identity is not None and identity_scope:
cg = container_group_client.get(resource_group_name, name)
_create_update_msi_role_assignment(cmd, resource_group_name, name, cg.identity.principal_id,
identity_role_id, identity_scope)
return lro


def _build_identities_info(identities):
Expand All @@ -234,6 +245,25 @@ def _build_identities_info(identities):
return identity


def _create_update_msi_role_assignment(cmd, resource_group_name, cg_name, identity_principle_id,
role_definition_id, identity_scope):
from azure.cli.core.profiles import ResourceType, get_sdk

RoleAssignmentCreateParameters = get_sdk(cmd.cli_ctx, ResourceType.MGMT_AUTHORIZATION,
'RoleAssignmentCreateParameters',
mod='models', operation_group='role_assignments')

assignment_client = get_auth_management_client(cmd.cli_ctx, identity_scope).role_assignments

role_assignment_guid = str(_gen_guid())

create_params = RoleAssignmentCreateParameters(
role_definition_id=role_definition_id,
principal_id=identity_principle_id
)
return assignment_client.create(identity_scope, role_assignment_guid, create_params, custom_headers=None)


def _get_resource(client, resource_group_name, *subresources):
from msrestazure.azure_exceptions import CloudError
try:
Expand Down Expand Up @@ -801,3 +831,8 @@ def _move_console_cursor_up(lines):
if lines > 0:
# Use stdout.write to support Python 2
sys.stdout.write('\033[{}A\033[K\033[J'.format(lines))


def _gen_guid():
import uuid
return uuid.uuid4()
Loading

0 comments on commit 8c0e296

Please sign in to comment.