Hello community, here is the log from the commit of package azure-cli-role for openSUSE:Factory checked in at 2018-05-13 16:03:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/azure-cli-role (Old) and /work/SRC/openSUSE:Factory/.azure-cli-role.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "azure-cli-role" Sun May 13 16:03:09 2018 rev:3 rq:600064 version:2.0.22 Changes: -------- --- /work/SRC/openSUSE:Factory/azure-cli-role/azure-cli-role.changes 2018-02-14 09:31:55.670815092 +0100 +++ /work/SRC/openSUSE:Factory/.azure-cli-role.new/azure-cli-role.changes 2018-05-13 16:03:09.862492425 +0200 @@ -1,0 +2,8 @@ +Fri Apr 20 11:12:55 UTC 2018 - adrian.glaub...@suse.com + +- New upstream release + + Version 2.0.22 +- Move LICENSE.txt from %doc to %license section +- Update Requires from setup.py + +------------------------------------------------------------------- Old: ---- azure-cli-role-2.0.17.tar.gz New: ---- azure-cli-role-2.0.22.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ azure-cli-role.spec ++++++ --- /var/tmp/diff_new_pack.0V0ymc/_old 2018-05-13 16:03:10.490469514 +0200 +++ /var/tmp/diff_new_pack.0V0ymc/_new 2018-05-13 16:03:10.494469368 +0200 @@ -17,7 +17,7 @@ Name: azure-cli-role -Version: 2.0.17 +Version: 2.0.22 Release: 0 Summary: Microsoft Azure CLI 'role' Command Module for Role-Based Access Control (RBAC) License: MIT @@ -34,10 +34,12 @@ Requires: azure-cli-command-modules-nspkg Requires: azure-cli-core Requires: azure-cli-nspkg -Requires: python3-azure-graphrbac >= 0.31.0 +Requires: python3-azure-graphrbac >= 0.40.0 Requires: python3-azure-keyvault >= 0.3.7 -Requires: python3-azure-mgmt-authorization >= 0.30.0 +Requires: python3-azure-mgmt-authorization >= 0.40.0 +Requires: python3-azure-mgmt-monitor >= 0.5.0 Requires: python3-azure-nspkg +Requires: python3-pytz Conflicts: azure-cli < 2.0.0 BuildArch: noarch @@ -66,7 +68,8 @@ %files %defattr(-,root,root,-) -%doc HISTORY.rst LICENSE.txt README.rst +%doc HISTORY.rst README.rst +%license LICENSE.txt %{python3_sitelib}/azure/cli/command_modules/role %{python3_sitelib}/azure_cli_role-*.egg-info ++++++ azure-cli-role-2.0.17.tar.gz -> azure-cli-role-2.0.22.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/HISTORY.rst new/azure-cli-role-2.0.22/HISTORY.rst --- old/azure-cli-role-2.0.17/HISTORY.rst 2018-01-12 18:25:22.000000000 +0100 +++ new/azure-cli-role-2.0.22/HISTORY.rst 2018-04-06 19:33:13.000000000 +0200 @@ -3,6 +3,27 @@ Release History =============== +2.0.22 +++++++ + +* `sdist` is now compatible with wheel 0.31.0 + +2.0.21 +++++++ +* graph: support required access configuration and native client +* rbac: ensure collection has less than 1000 ids on resolving graph objects +* ad sp: new commands to manage credentials "az ad sp credential reset/list/delete" +* role assignments: (breaking change)list/show output has "properties" removed to align with SDK +* role definition: support `dataActions` and `notDataActions` + +2.0.20 +++++++ +* role assignments: expose "role assignment list-changelogs" for rbac audit + +2.0.18 +++++++ +* ad app update: expose "--available-to-other-tenants" + 2.0.17 ++++++ * role assignment: expose --assignee-object-id to bypass graph query diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/PKG-INFO new/azure-cli-role-2.0.22/PKG-INFO --- old/azure-cli-role-2.0.17/PKG-INFO 2018-01-12 18:25:50.000000000 +0100 +++ new/azure-cli-role-2.0.22/PKG-INFO 2018-04-06 19:33:56.000000000 +0200 @@ -1,11 +1,12 @@ Metadata-Version: 1.1 Name: azure-cli-role -Version: 2.0.17 +Version: 2.0.22 Summary: Microsoft Azure Command-Line Tools Role Command Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation Author-email: azpy...@microsoft.com License: MIT +Description-Content-Type: UNKNOWN Description: Microsoft Azure CLI 'role' Command Module for Role-Based Access Control (RBAC) ============================================================================== @@ -20,6 +21,27 @@ Release History =============== + 2.0.22 + ++++++ + + * `sdist` is now compatible with wheel 0.31.0 + + 2.0.21 + ++++++ + * graph: support required access configuration and native client + * rbac: ensure collection has less than 1000 ids on resolving graph objects + * ad sp: new commands to manage credentials "az ad sp credential reset/list/delete" + * role assignments: (breaking change)list/show output has "properties" removed to align with SDK + * role definition: support `dataActions` and `notDataActions` + + 2.0.20 + ++++++ + * role assignments: expose "role assignment list-changelogs" for rbac audit + + 2.0.18 + ++++++ + * ad app update: expose "--available-to-other-tenants" + 2.0.17 ++++++ * role assignment: expose --assignee-object-id to bypass graph query diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/azure/cli/command_modules/role/_help.py new/azure-cli-role-2.0.22/azure/cli/command_modules/role/_help.py --- old/azure-cli-role-2.0.17/azure/cli/command_modules/role/_help.py 2018-01-12 18:25:22.000000000 +0100 +++ new/azure-cli-role-2.0.22/azure/cli/command_modules/role/_help.py 2018-04-06 19:33:13.000000000 +0200 @@ -57,8 +57,28 @@ text: az ad sp create-for-rbac --keyvault MyVault --cert CertName """ +helps['ad sp credential'] = """ + type: group + short-summary: manage a service principal's credentials. +""" + +helps['ad sp credential list'] = """ + type: command + short-summary: list a service principal's credentials. +""" + +helps['ad sp credential delete'] = """ + type: command + short-summary: delete a service principal's credential. +""" + helps['ad sp reset-credentials'] = """ type: command + short-summary: (Deprecated, use "az ad sp credential reset") +""" + +helps['ad sp credential reset'] = """ + type: command short-summary: Reset a service principal credential. long-summary: Use upon expiration of the service principal's credentials, or in the event that login credentials are lost. parameters: @@ -175,6 +195,12 @@ "Microsoft.Insights/alertRules/*", "Microsoft.Support/*" ], + "DataActions": [ + "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/*" + ], + "NotDataActions": [ + "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write" + ], "AssignableScopes": ["/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"] }' - name: Create a role from a file containing a JSON description. @@ -215,6 +241,12 @@ "Microsoft.Insights/alertRules/*", "Microsoft.Support/*" ], + "DataActions": [ + "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/*" + ], + "NotDataActions": [ + "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write" + ], "AssignableScopes": ["/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"] }' - name: Create a role from a file containing a JSON description. @@ -223,11 +255,25 @@ """ helps['ad'] = """ type: group - short-summary: Synchronize on-premises directories and manage Azure Active Directory resources. + short-summary: Manage Azure Active Directory Graph entities needed for Role Based Access Control """ -helps['ad app'] = """ - type: group - short-summary: Manage Azure Active Directory applications. +helps['ad app create'] = """ + type: command + short-summary: Create a web application, web API or native application + examples: + - name: Create a native application with delegated permission of "access the AAD directory as the signed-in user + text: | + az ad app create --display-name my-native --native-app --requiredResourceAccess @manifest.json + ("manifest.json" contains the following content) + [{ + "resourceAppId": "00000002-0000-0000-c000-000000000000", + "resourceAccess": [ + { + "id": "a42657d6-7f20-40e3-b6f0-cee03008a62a", + "type": "Scope" + } + ] + }] """ helps['ad group'] = """ type: group diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/azure/cli/command_modules/role/_params.py new/azure-cli-role-2.0.22/azure/cli/command_modules/role/_params.py --- old/azure-cli-role-2.0.17/azure/cli/command_modules/role/_params.py 2018-01-12 18:25:22.000000000 +0100 +++ new/azure-cli-role-2.0.22/azure/cli/command_modules/role/_params.py 2018-04-06 19:33:13.000000000 +0200 @@ -8,11 +8,11 @@ from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import get_enum_type, get_three_state_flag +from azure.cli.core.commands.validators import validate_file_or_dict from azure.cli.command_modules.role._completers import get_role_definition_name_completion_list from azure.cli.command_modules.role._validators import validate_group, validate_member_id, validate_cert, VARIANT_GROUP_ID_ARGS - name_arg_type = CLIArgumentType(options_list=('--name', '-n'), metavar='NAME') @@ -24,8 +24,8 @@ c.argument('display_name', help='the display name of the application') c.argument('homepage', help='the url where users can sign in and use your app.') c.argument('identifier', options_list=['--id'], help='identifier uri, application id, or object id') - c.argument('identifier_uris', nargs='+', help='space separated unique URIs that Azure AD can use for this app.') - c.argument('reply_urls', nargs='+', help='space separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. The value does not need to be a physical endpoint, but must be a valid URI.') + c.argument('identifier_uris', nargs='+', help='space-separated unique URIs that Azure AD can use for this app.') + c.argument('reply_urls', nargs='+', help='space-separated URIs to which Azure AD will redirect in response to an OAuth 2.0 request. The value does not need to be a physical endpoint, but must be a valid URI.') c.argument('start_date', help="Date or datetime at which credentials become valid(e.g. '2017-01-01T01:00:00+00:00' or '2017-01-01'). Default value is current time") c.argument('end_date', help="Date or datetime after which credentials expire(e.g. '2017-12-31T11:59:59+00:00' or '2017-12-31'). Default value is one year after current time") c.argument('available_to_other_tenants', help='the application can be used from any Azure AD tenants', arg_type=get_three_state_flag()) @@ -33,6 +33,14 @@ # TODO: Update these with **enum_choice_list(...) when SDK supports proper enums c.argument('key_type', help='the type of the key credentials associated with the application', arg_type=get_enum_type(['AsymmetricX509Cert', 'Password', 'Symmetric'], default='AsymmetricX509Cert')) c.argument('key_usage', help='the usage of the key credentials associated with the application.', arg_type=get_enum_type(['Sign', 'Verify'], default='Verify')) + c.argument('password', help="app password, aka 'client secret'") + c.argument('oauth2_allow_implicit_flow', arg_type=get_three_state_flag(), help='whether to allow implicit grant flow for OAuth2') + c.argument('required_resource_accesses', type=validate_file_or_dict, + help="resource scopes and roles the application requires access to. Should be in manifest json format. See examples below for details") + c.argument('native_app', arg_type=get_three_state_flag(), help="an application which can be installed on a user's device or computer") + + with self.argument_context('ad') as c: + c.ignore('additional_properties') with self.argument_context('ad sp') as c: c.argument('identifier', options_list=['--id'], help='service principal name, or object id') @@ -46,7 +54,7 @@ c.argument('skip_assignment', arg_type=get_three_state_flag(), help='do not create default assignment') c.argument('show_auth_for_sdk', options_list='--sdk-auth', help='output result in compatible with Azure SDK auth file', arg_type=get_three_state_flag()) - for item in ['create-for-rbac', 'reset-credentials']: + for item in ['create-for-rbac', 'reset-credentials', 'ad sp']: with self.argument_context('ad sp {}'.format(item)) as c: c.argument('name', name_arg_type) c.argument('cert', arg_group='Credential', validator=validate_cert) @@ -99,11 +107,17 @@ c.argument('role', help='role name or id', completer=get_role_definition_name_completion_list) c.argument('show_all', options_list=['--all'], action='store_true', help='show all assignments under the current subscription') c.argument('include_inherited', action='store_true', help='include assignments applied on parent scopes') + c.argument('can_delegate', action='store_true', help='when set, the assignee will be able to create further role assignments to the same role') c.argument('assignee', help='represent a user, group, or service principal. supported format: object id, user sign-in name, or service principal name') c.argument('assignee_object_id', help="assignee's graph object id, such as the 'principal id' from a managed service identity. Use this instead of '--assignee' to bypass graph permission issues") - c.argument('ids', nargs='+', help='space separated role assignment ids') + c.argument('ids', nargs='+', help='space-separated role assignment ids') c.argument('include_classic_administrators', arg_type=get_three_state_flag(), help='list default role assignments for subscription classic administrators, aka co-admins') + time_help = ('The {} of the query in the format of %Y-%m-%dT%H:%M:%SZ, e.g. 2000-12-31T12:59:59Z. Defaults to {}') + with self.argument_context('role assignment list-changelogs') as c: + c.argument('start_time', help=time_help.format('start time', '1 Hour prior to the current time')) + c.argument('end_time', help=time_help.format('end time', 'the current time')) + with self.argument_context('role definition') as c: c.argument('role_definition_id', options_list=['--name', '-n'], help='the role definition name') c.argument('custom_role_only', arg_type=get_three_state_flag(), help='custom roles only(vs. build-in ones)') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/azure/cli/command_modules/role/commands.py new/azure-cli-role-2.0.22/azure/cli/command_modules/role/commands.py --- old/azure-cli-role-2.0.17/azure/cli/command_modules/role/commands.py 2018-01-12 18:25:22.000000000 +0100 +++ new/azure-cli-role-2.0.22/azure/cli/command_modules/role/commands.py 2018-04-06 19:33:13.000000000 +0200 @@ -15,14 +15,14 @@ def transform_definition_list(result): - return [OrderedDict([('Name', r['properties']['roleName']), ('Type', r['properties']['type']), - ('Description', r['properties']['description'])]) for r in result] + return [OrderedDict([('Name', r['roleName']), ('Type', r['type']), + ('Description', r['description'])]) for r in result] def transform_assignment_list(result): - return [OrderedDict([('Principal', r['properties']['principalName']), - ('Role', r['properties']['roleDefinitionName']), - ('Scope', r['properties']['scope'])]) for r in result] + return [OrderedDict([('Principal', r['principalName']), + ('Role', r['roleDefinitionName']), + ('Scope', r['scope'])]) for r in result] def get_role_definition_op(operation_name): @@ -73,6 +73,7 @@ g.custom_command('delete', 'delete_role_assignments') g.custom_command('list', 'list_role_assignments', table_transformer=transform_assignment_list) g.custom_command('create', 'create_role_assignment') + g.custom_command('list-changelogs', 'list_role_assignment_change_logs') with self.command_group('ad app', client_factory=get_graph_client_applications, resource_type=PROFILE_TYPE, min_api='2017-03-10') as g: g.custom_command('create', 'create_application') @@ -90,7 +91,10 @@ # RBAC related with self.command_group('ad sp') as g: g.custom_command('create-for-rbac', 'create_service_principal_for_rbac') - g.custom_command('reset-credentials', 'reset_service_principal_credential') + g.custom_command('reset-credentials', 'reset_service_principal_credential', deprecate_info='ad sp credential reset') + g.custom_command('credential reset', 'reset_service_principal_credential') + g.custom_command('credential list', 'list_service_principal_credentials') + g.custom_command('credential delete', 'delete_service_principal_credential') with self.command_group('ad user', role_users_sdk) as g: g.command('delete', 'delete') @@ -99,7 +103,7 @@ g.custom_command('create', 'create_user', client_factory=get_graph_client_users, doc_string_source='azure.graphrbac.models#UserCreateParameters') with self.command_group('ad group', role_group_sdk) as g: - g.command('create', 'create') + g.custom_command('create', 'create_group', client_factory=get_graph_client_groups) g.command('delete', 'delete') g.command('show', 'get', exception_handler=empty_on_404) g.command('get-member-groups', 'get_member_groups') @@ -109,4 +113,4 @@ g.command('list', 'get_group_members') g.command('add', 'add_member') g.command('remove', 'remove_member') - g.command('check', 'is_member_of') + g.custom_command('check', 'check_group_membership', client_factory=get_graph_client_groups) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/azure/cli/command_modules/role/custom.py new/azure-cli-role-2.0.22/azure/cli/command_modules/role/custom.py --- old/azure-cli-role-2.0.17/azure/cli/command_modules/role/custom.py 2018-01-12 18:25:22.000000000 +0100 +++ new/azure-cli-role-2.0.22/azure/cli/command_modules/role/custom.py 2018-04-06 19:33:13.000000000 +0200 @@ -6,6 +6,7 @@ from __future__ import print_function import datetime +import json import re import os import uuid @@ -20,16 +21,12 @@ from azure.cli.core.util import get_file_json, shell_safe_json_parse -from azure.mgmt.authorization.models import (RoleAssignmentProperties, Permission, RoleDefinition, - RoleDefinitionProperties) +from azure.mgmt.authorization.models import RoleAssignmentCreateParameters, Permission, RoleDefinition -from azure.graphrbac.models import (ApplicationCreateParameters, - ApplicationUpdateParameters, - PasswordCredential, - KeyCredential, - UserCreateParameters, - PasswordProfile, - ServicePrincipalCreateParameters) +from azure.graphrbac.models import (ApplicationCreateParameters, ApplicationUpdateParameters, PasswordCredential, + KeyCredential, UserCreateParameters, PasswordProfile, + ServicePrincipalCreateParameters, RequiredResourceAccess, + ResourceAccess, GroupCreateParameters, CheckGroupMembershipParameters) from ._client_factory import _auth_client_factory, _graph_client_factory @@ -81,7 +78,7 @@ raise CLIError('Please provide the unique logic name of an existing role') role_definition['name'] = matched[0].name # ensure correct logical name and guid name. For update we accept both - role_name = matched[0].properties.role_name + role_name = matched[0].role_name role_id = matched[0].name else: role_id = _gen_guid() @@ -90,18 +87,19 @@ raise CLIError("please provide 'assignableScopes'") permission = Permission(actions=role_definition.get('actions', None), - not_actions=role_definition.get('notActions', None)) - properties = RoleDefinitionProperties(role_name=role_name, - description=role_definition.get('description', None), - type=_CUSTOM_RULE, - assignable_scopes=role_definition['assignableScopes'], - permissions=[permission]) - - definition = RoleDefinition(name=role_id, properties=properties) + not_actions=role_definition.get('notActions', None), + data_actions=role_definition.get('dataActions', None), + not_data_actions=role_definition.get('notDataActions', None)) + + role_definition = RoleDefinition(role_name=role_name, + description=role_definition.get('description', None), + role_type=_CUSTOM_RULE, + assignable_scopes=role_definition['assignableScopes'], + permissions=[permission]) return definitions_client.create_or_update(role_definition_id=role_id, - scope=properties.assignable_scopes[0], - role_definition=definition) + scope=role_definition.assignable_scopes[0], + role_definition=role_definition) def delete_role_definition(cmd, name, resource_group_name=None, scope=None, @@ -117,17 +115,18 @@ def _search_role_definitions(definitions_client, name, scope, custom_role_only=False): roles = list(definitions_client.list(scope)) if name: - roles = [r for r in roles if r.name == name or r.properties.role_name == name] + roles = [r for r in roles if r.name == name or r.role_name == name] if custom_role_only: - roles = [r for r in roles if r.properties.type == _CUSTOM_RULE] + roles = [r for r in roles if r.role_type == _CUSTOM_RULE] return roles -def create_role_assignment(cmd, role, assignee=None, assignee_object_id=None, resource_group_name=None, scope=None): +def create_role_assignment(cmd, role, assignee=None, assignee_object_id=None, resource_group_name=None, + scope=None): if bool(assignee) == bool(assignee_object_id): raise CLIError('usage error: --assignee STRING | --assignee-object-id GUID') - return _create_role_assignment(cmd.cli_ctx, role, assignee or assignee_object_id, - resource_group_name, scope, resolve_assignee=(not assignee_object_id)) + return _create_role_assignment(cmd.cli_ctx, role, assignee or assignee_object_id, resource_group_name, scope, + resolve_assignee=(not assignee_object_id)) def _create_role_assignment(cli_ctx, role, assignee, resource_group_name=None, scope=None, @@ -141,10 +140,11 @@ role_id = _resolve_role_id(role, scope, definitions_client) object_id = _resolve_object_id(cli_ctx, assignee) if resolve_assignee else assignee - properties = RoleAssignmentProperties(role_id, object_id) + parameters = RoleAssignmentCreateParameters(role_definition_id=role_id, principal_id=object_id) assignment_name = _gen_guid() custom_headers = None - return assignments_client.create(scope, assignment_name, properties, + return assignments_client.create(scope=scope, role_assignment_name=assignment_name, + parameters=parameters, custom_headers=custom_headers) @@ -184,22 +184,22 @@ # 2. fill in role names role_defs = list(definitions_client.list( scope=scope or ('/subscriptions/' + definitions_client.config.subscription_id))) - role_dics = {i.id: i.properties.role_name for i in role_defs} + role_dics = {i.id: i.role_name for i in role_defs} for i in results: - if role_dics.get(i['properties']['roleDefinitionId']): - i['properties']['roleDefinitionName'] = role_dics[i['properties']['roleDefinitionId']] + if role_dics.get(i['roleDefinitionId']): + i['roleDefinitionName'] = role_dics[i['roleDefinitionId']] # fill in principal names - principal_ids = set(i['properties']['principalId'] for i in results if i['properties']['principalId']) + principal_ids = set(i['principalId'] for i in results if i['principalId']) if principal_ids: try: principals = _get_object_stubs(graph_client, principal_ids) principal_dics = {i.object_id: _get_displayable_name(i) for i in principals} - for i in [r for r in results if not r['properties'].get('principalName')]: - i['properties']['principalName'] = '' - if principal_dics.get(i['properties']['principalId']): - i['properties']['principalName'] = principal_dics[i['properties']['principalId']] + for i in [r for r in results if not r.get('principalName')]: + i['principalName'] = '' + if principal_dics.get(i['principalId']): + i['principalName'] = principal_dics[i['principalId']] except (CloudError, GraphErrorException) as ex: # failure on resolving principal due to graph permission should not fail the whole thing logger.info("Failed to resolve graph object information per error '%s'", ex) @@ -207,41 +207,173 @@ return results +def _get_assignment_events(cli_ctx, start_time=None, end_time=None): + from azure.mgmt.monitor import MonitorManagementClient + from azure.cli.core.commands.client_factory import get_mgmt_service_client + client = get_mgmt_service_client(cli_ctx, MonitorManagementClient) + DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' + if end_time: + try: + end_time = datetime.datetime.strptime(end_time, DATE_TIME_FORMAT) + except ValueError: + raise CLIError("Input '{}' is not valid datetime. Valid example: 2000-12-31T12:59:59Z".format(end_time)) + else: + end_time = datetime.datetime.utcnow() + + if start_time: + try: + start_time = datetime.datetime.strptime(start_time, DATE_TIME_FORMAT) + if start_time >= end_time: + raise CLIError("Start time cannot be later than end time.") + except ValueError: + raise CLIError("Input '{}' is not valid datetime. Valid example: 2000-12-31T12:59:59Z".format(start_time)) + else: + start_time = end_time - datetime.timedelta(hours=1) + + time_filter = 'eventTimestamp ge {} and eventTimestamp le {}'.format(start_time.strftime('%Y-%m-%dT%H:%M:%SZ'), + end_time.strftime('%Y-%m-%dT%H:%M:%SZ')) + + # set time range filter + odata_filters = 'resourceProvider eq Microsoft.Authorization and {}'.format(time_filter) + + activity_log = list(client.activity_logs.list(filter=odata_filters)) + start_events, end_events, offline_events = {}, {}, [] + + for l in activity_log: + if l.http_request: + if l.status.value == 'Started': + start_events[l.operation_id] = l + else: + end_events[l.operation_id] = l + elif l.event_name and l.event_name.value.lower() == 'classicadministrators': + offline_events.append(l) + return start_events, end_events, offline_events, client + + +# A custom command around 'monitoring' events to produce understandable output for RBAC audit, a common scenario. +def list_role_assignment_change_logs(cmd, start_time=None, end_time=None): + # pylint: disable=too-many-nested-blocks, too-many-statements + result = [] + start_events, end_events, offline_events, client = _get_assignment_events(cmd.cli_ctx, start_time, end_time) + role_defs = {d.id: [d.role_name, d.id.split('/')[-1]] for d in list_role_definitions(cmd)} + + for op_id in start_events: + e = end_events.get(op_id, None) + if not e: + continue + + entry = {} + op = e.operation_name and e.operation_name.value + if (op.lower().startswith('microsoft.authorization/roleassignments') and e.status.value == 'Succeeded'): + s, payload = start_events[op_id], None + entry = dict.fromkeys( + ['principalId', 'principalName', 'scope', 'scopeName', 'scopeType', 'roleDefinitionId', 'roleName'], + None) + entry['timestamp'], entry['caller'] = e.event_timestamp, s.caller + + if s.http_request: + if s.http_request.method == 'PUT': + # 'requestbody' has a wrong camel-case. Should be 'requestBody' + payload = s.properties and s.properties.get('requestbody') + entry['action'] = 'Granted' + entry['scope'] = e.authorization.scope + elif s.http_request.method == 'DELETE': + payload = e.properties and e.properties.get('responseBody') + entry['action'] = 'Revoked' + if payload: + try: + payload = json.loads(payload) + except ValueError: + pass + if payload: + payload = payload['properties'] + entry['principalId'] = payload['principalId'] + if not entry['scope']: + entry['scope'] = payload['scope'] + if entry['scope']: + index = entry['scope'].lower().find('/providers/microsoft.authorization') + if index != -1: + entry['scope'] = entry['scope'][:index] + parts = list(filter(None, entry['scope'].split('/'))) + entry['scopeName'] = parts[-1] + if len(parts) < 3: + entry['scopeType'] = 'Subscription' + elif len(parts) < 5: + entry['scopeType'] = 'Resource group' + else: + entry['scopeType'] = 'Resource' + + entry['roleDefinitionId'] = role_defs[payload['roleDefinitionId']][1] + entry['roleName'] = role_defs[payload['roleDefinitionId']][0] + result.append(entry) + + # Fill in logical user/sp names as guid principal-id not readable + principal_ids = set([x['principalId'] for x in result if x['principalId']]) + if principal_ids: + graph_client = _graph_client_factory(cmd.cli_ctx) + stubs = _get_object_stubs(graph_client, principal_ids) + principal_dics = {i.object_id: _get_displayable_name(i) for i in stubs} + if principal_dics: + for e in result: + e['principalName'] = principal_dics.get(e['principalId'], None) + + offline_events = [x for x in offline_events if (x.status and x.status.value == 'Succeeded' and x.operation_name and + x.operation_name.value.lower().startswith( + 'microsoft.authorization/classicadministrators'))] + for e in offline_events: + entry = { + 'timestamp': e.event_timestamp, + 'caller': 'Subscription Admin', + 'roleDefinitionId': None, + 'principalId': None, + 'principalType': 'User', + 'scope': '/subscriptions/' + client.config.subscription_id, + 'scopeType': 'Subscription', + 'scopeName': client.config.subscription_id, + } + if e.properties: + entry['principalName'] = e.properties.get('adminEmail') + entry['roleName'] = e.properties.get('adminType') + result.append(entry) + + return result + + def _backfill_assignments_for_co_admins(cli_ctx, auth_client, assignee=None): - co_admins = auth_client.classic_administrators.list('2015-06-01') # known swagger bug on api-version handling - co_admins = [x for x in co_admins if x.properties.email_address] + co_admins = auth_client.classic_administrators.list() # known swagger bug on api-version handling + co_admins = [x for x in co_admins if x.email_address] graph_client = _graph_client_factory(cli_ctx) if assignee: # apply assignee filter if applicable if _is_guid(assignee): - result = _get_object_stubs(graph_client, [assignee]) - if not result: - return [] - assignee = _get_displayable_name(result[0]).lower() - - co_admins = [x for x in co_admins if assignee == x.properties.email_address.lower()] + try: + result = _get_object_stubs(graph_client, [assignee]) + if not result: + return [] + assignee = _get_displayable_name(result[0]).lower() + except ValueError: + pass + co_admins = [x for x in co_admins if assignee == x.email_address.lower()] if not co_admins: return [] result, users = [], [] for i in range(0, len(co_admins), 10): # graph allows up to 10 query filters, so split into chunks here - upn_queries = ["userPrincipalName eq '{}'".format(x.properties.email_address) for x in co_admins[i:i + 10]] + upn_queries = ["userPrincipalName eq '{}'".format(x.email_address) for x in co_admins[i:i + 10]] temp = list(list_users(graph_client.users, query_filter=' or '.join(upn_queries))) users += temp upns = {u.user_principal_name: u.object_id for u in users} for admin in co_admins: na_text = 'NA(classic admins)' - email = admin.properties.email_address + email = admin.email_address result.append({ 'id': na_text, 'name': na_text, - 'properties': { - 'principalId': upns.get(email), - 'principalName': email, - 'roleDefinitionName': admin.properties.role, - 'roleDefinitionId': 'NA(classic admin role)', - 'scope': '/subscriptions/' + auth_client.config.subscription_id - } + 'principalId': upns.get(email), + 'principalName': email, + 'roleDefinitionName': admin.role, + 'roleDefinitionId': 'NA(classic admin role)', + 'scope': '/subscriptions/' + auth_client.config.subscription_id }) return result @@ -301,13 +433,13 @@ if assignments: assignments = [a for a in assignments if ( not scope or - include_inherited and re.match(a.properties.scope, scope, re.I) or - a.properties.scope.lower() == scope.lower() + include_inherited and re.match(a.scope, scope, re.I) or + a.scope.lower() == scope.lower() )] if role: role_id = _resolve_role_id(role, scope, definitions_client) - assignments = [i for i in assignments if i.properties.role_definition_id == role_id] + assignments = [i for i in assignments if i.role_definition_id == role_id] return assignments @@ -394,10 +526,20 @@ display_name=display_name, mail_nickname=mail_nickname, immutable_id=immutable_id, password_profile=PasswordProfile( - password, force_change_password_next_login)) + password=password, + force_change_password_next_login=force_change_password_next_login)) return client.create(param) +def create_group(client, display_name, mail_nickname): + return client.create(GroupCreateParameters(display_name=display_name, mail_nickname=mail_nickname)) + + +def check_group_membership(cmd, client, group_id, member_object_id): # pylint: disable=unused-argument + return client.is_member_of(CheckGroupMembershipParameters(group_id=group_id, + member_id=member_object_id)) + + def list_groups(client, display_name=None, query_filter=None): ''' list groups in the directory @@ -410,23 +552,36 @@ return client.list(filter=(' and ').join(sub_filters)) -def create_application(client, display_name, homepage, identifier_uris, +def create_application(client, display_name, homepage=None, identifier_uris=None, available_to_other_tenants=False, password=None, reply_urls=None, - key_value=None, key_type=None, key_usage=None, start_date=None, - end_date=None): - password_creds, key_creds = _build_application_creds(password, key_value, key_type, - key_usage, start_date, end_date) - - app_create_param = ApplicationCreateParameters(available_to_other_tenants, - display_name, - identifier_uris, - homepage=homepage, - reply_urls=reply_urls, - key_credentials=key_creds, - password_credentials=password_creds) + key_value=None, key_type=None, key_usage=None, start_date=None, end_date=None, + oauth2_allow_implicit_flow=None, required_resource_accesses=None, native_app=None): + key_creds, password_creds, required_accesses = None, None, None + if native_app: + if identifier_uris: + raise CLIError("'--identifier-uris' is not required for creating a native application") + identifier_uris = ['http://{}'.format(_gen_guid())] # we will create a temporary one and remove it later + else: + if not identifier_uris: + raise CLIError("'--identifier-uris' is required for creating an application") + password_creds, key_creds = _build_application_creds(password, key_value, key_type, + key_usage, start_date, end_date) + + if required_resource_accesses: + required_accesses = _build_application_accesses(required_resource_accesses) + + app_patch_param = ApplicationCreateParameters(available_to_other_tenants=available_to_other_tenants, + display_name=display_name, + identifier_uris=identifier_uris, + homepage=homepage, + reply_urls=reply_urls, + key_credentials=key_creds, + password_credentials=password_creds, + oauth2_allow_implicit_flow=oauth2_allow_implicit_flow, + required_resource_access=required_accesses) try: - return client.create(app_create_param) + result = client.create(app_patch_param) except GraphErrorException as ex: if 'insufficient privileges' in str(ex).lower(): link = 'https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal' # pylint: disable=line-too-long @@ -434,23 +589,57 @@ "For how to configure, please refer '{}'. Original error: {}".format(link, ex)) raise + if native_app: + # AAD graph doesn't have the API to create a native app, aka public client, the recommended hack is + # to create a web app first, then convert to a native one + # pylint: disable=protected-access + if 'public_client' not in ApplicationUpdateParameters._attribute_map: + ApplicationUpdateParameters._attribute_map['public_client'] = {'key': 'publicClient', 'type': 'bool'} + app_patch_param = ApplicationUpdateParameters(identifier_uris=[]) + setattr(app_patch_param, 'public_client', True) + client.patch(result.object_id, app_patch_param) + result = client.get(result.object_id) + + return result + def update_application(client, identifier, display_name=None, homepage=None, identifier_uris=None, password=None, reply_urls=None, key_value=None, - key_type=None, key_usage=None, start_date=None, end_date=None): + key_type=None, key_usage=None, start_date=None, end_date=None, available_to_other_tenants=None, + oauth2_allow_implicit_flow=None, required_resource_accesses=None): object_id = _resolve_application(client, identifier) - password_creds, key_creds = _build_application_creds(password, key_value, key_type, - key_usage, start_date, end_date) + + password_creds, key_creds, required_accesses = None, None, None + if any([key_value, key_type, key_usage, start_date, end_date]): + password_creds, key_creds = _build_application_creds(password, key_value, key_type, + key_usage, start_date, end_date) + + if required_resource_accesses: + required_accesses = _build_application_accesses(required_resource_accesses) app_patch_param = ApplicationUpdateParameters(display_name=display_name, homepage=homepage, identifier_uris=identifier_uris, reply_urls=reply_urls, key_credentials=key_creds, - password_credentials=password_creds) + password_credentials=password_creds, + available_to_other_tenants=available_to_other_tenants, + required_resource_access=required_accesses, + oauth2_allow_implicit_flow=oauth2_allow_implicit_flow) return client.patch(object_id, app_patch_param) +def _build_application_accesses(required_resource_accesses): + required_accesses = None + for x in required_resource_accesses: + accesses = [ResourceAccess(id=y['id'], type=y['type']) for y in x['resourceAccess']] + if required_accesses is None: + required_accesses = [] + required_accesses.append(RequiredResourceAccess(resource_app_id=x['resourceAppId'], + resource_access=accesses)) + return required_accesses + + def show_application(client, identifier): object_id = _resolve_application(client, identifier) return client.get(object_id) @@ -494,10 +683,12 @@ password_creds = None key_creds = None if password: - password_creds = [PasswordCredential(start_date, end_date, str(_gen_guid()), password)] + password_creds = [PasswordCredential(start_date=start_date, end_date=end_date, + key_id=str(_gen_guid()), value=password)] elif key_value: - key_creds = [KeyCredential(start_date, end_date, key_value, str(_gen_guid()), - key_usage, key_type)] + key_creds = [KeyCredential(start_date=start_date, end_date=end_date, + key_id=str(_gen_guid()), value=key_value, + usage=key_usage, type=key_type)] return (password_creds, key_creds) @@ -532,25 +723,86 @@ def delete_service_principal(cmd, identifier): client = _graph_client_factory(cmd.cli_ctx) - sp = client.service_principals.get(_resolve_service_principal(client.service_principals, identifier)) + sp_object_id = _resolve_service_principal(client.service_principals, identifier) + app_object_id = _get_app_object_id_from_sp_object_id(client, sp_object_id) + + assignments = list_role_assignments(cmd, assignee=identifier, show_all=True) + if assignments: + logger.warning('Removing role assignments') + delete_role_assignments(cmd, [a['id'] for a in assignments]) + + if app_object_id: # delete the application, and AAD service will automatically clean up the SP + client.applications.delete(app_object_id) + else: + client.service_principals.delete(sp_object_id) + + +def _get_app_object_id_from_sp_object_id(client, sp_object_id): + sp = client.service_principals.get(sp_object_id) app_object_id = None - # see whether we need to delete the application if it is in the same tenant if sp.service_principal_names: result = list(client.applications.list( filter="identifierUris/any(s:s eq '{}')".format(sp.service_principal_names[0]))) if result: app_object_id = result[0].object_id + return app_object_id - assignments = list_role_assignments(cmd, assignee=identifier, show_all=True) - if assignments: - logger.warning('Removing role assignments') - delete_role_assignments(cmd, [a['id'] for a in assignments]) - if app_object_id: # delete the application, and AAD service will automatically clean up the SP - client.applications.delete(app_object_id) +def list_service_principal_credentials(cmd, identifier, cert=False): + client = _graph_client_factory(cmd.cli_ctx) + sp_object_id = _resolve_service_principal(client.service_principals, identifier) + app_object_id = _get_app_object_id_from_sp_object_id(client, sp_object_id) + sp_creds, app_creds = [], [] + if cert: + sp_creds = list(client.service_principals.list_key_credentials(sp_object_id)) + if app_object_id: + app_creds = list(client.applications.list_key_credentials(app_object_id)) else: - client.service_principals.delete(sp.object_id) + sp_creds = list(client.service_principals.list_password_credentials(sp_object_id)) + if app_object_id: + app_creds = list(client.applications.list_password_credentials(app_object_id)) + + for x in sp_creds: + setattr(x, 'source', 'ServicePrincipal') + for x in app_creds: + setattr(x, 'source', 'Application') + return app_creds + sp_creds + + +def delete_service_principal_credential(cmd, identifier, key_id, cert=False): + client = _graph_client_factory(cmd.cli_ctx) + sp_object_id = _resolve_service_principal(client.service_principals, identifier) + if cert: + result = list(client.service_principals.list_key_credentials(sp_object_id)) + else: + result = list(client.service_principals.list_password_credentials(sp_object_id)) + + to_delete = next((x for x in result if x.key_id == key_id), None) + + # we will try to delete the creds at service principal level, if not found, we try application level + + if to_delete: + result.remove(to_delete) + if cert: + return client.service_principals.update_key_credentials(sp_object_id, result) + return client.service_principals.update_password_credentials(sp_object_id, result) + else: + app_object_id = _get_app_object_id_from_sp_object_id(client, sp_object_id) + if app_object_id: + if cert: + result = list(client.applications.list_key_credentials(app_object_id)) + else: + result = list(client.applications.list_password_credentials(app_object_id)) + to_delete = next((x for x in result if x.key_id == key_id), None) + if to_delete: + result.remove(to_delete) + if cert: + return client.applications.update_key_credentials(app_object_id, result) + return client.applications.update_password_credentials(app_object_id, result) + + raise CLIError("'{}' doesn't exist in the service principal of '{}' or associated application".format( + key_id, identifier)) def _resolve_service_principal(client, identifier): @@ -718,7 +970,6 @@ raise if show_auth_for_sdk: - import json from azure.cli.core._profile import Profile profile = Profile(cli_ctx=cmd.cli_ctx) result = profile.get_sp_auth_info(scopes[0].split('/')[2] if scopes else None, @@ -1001,12 +1252,16 @@ return False +def _get_object_stubs(graph_client, assignees): + from azure.graphrbac.models import GetObjectsParameters + result = [] + assignees = list(assignees) # callers could pass in a set + for i in range(0, len(assignees), 1000): + params = GetObjectsParameters(include_directory_object_references=True, object_ids=assignees[i:i + 1000]) + result += list(graph_client.objects.get_objects_by_object_ids(params)) + return result + + # for injecting test seams to produce predicatable role assignment id for playback def _gen_guid(): return uuid.uuid4() - - -def _get_object_stubs(graph_client, assignees): - from azure.graphrbac.models import GetObjectsParameters - params = GetObjectsParameters(include_directory_object_references=True, object_ids=assignees) - return list(graph_client.objects.get_objects_by_object_ids(params)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/azure_cli_role.egg-info/PKG-INFO new/azure-cli-role-2.0.22/azure_cli_role.egg-info/PKG-INFO --- old/azure-cli-role-2.0.17/azure_cli_role.egg-info/PKG-INFO 2018-01-12 18:25:50.000000000 +0100 +++ new/azure-cli-role-2.0.22/azure_cli_role.egg-info/PKG-INFO 2018-04-06 19:33:56.000000000 +0200 @@ -1,11 +1,12 @@ Metadata-Version: 1.1 Name: azure-cli-role -Version: 2.0.17 +Version: 2.0.22 Summary: Microsoft Azure Command-Line Tools Role Command Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation Author-email: azpy...@microsoft.com License: MIT +Description-Content-Type: UNKNOWN Description: Microsoft Azure CLI 'role' Command Module for Role-Based Access Control (RBAC) ============================================================================== @@ -20,6 +21,27 @@ Release History =============== + 2.0.22 + ++++++ + + * `sdist` is now compatible with wheel 0.31.0 + + 2.0.21 + ++++++ + * graph: support required access configuration and native client + * rbac: ensure collection has less than 1000 ids on resolving graph objects + * ad sp: new commands to manage credentials "az ad sp credential reset/list/delete" + * role assignments: (breaking change)list/show output has "properties" removed to align with SDK + * role definition: support `dataActions` and `notDataActions` + + 2.0.20 + ++++++ + * role assignments: expose "role assignment list-changelogs" for rbac audit + + 2.0.18 + ++++++ + * ad app update: expose "--available-to-other-tenants" + 2.0.17 ++++++ * role assignment: expose --assignee-object-id to bypass graph query diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/azure_cli_role.egg-info/requires.txt new/azure-cli-role-2.0.22/azure_cli_role.egg-info/requires.txt --- old/azure-cli-role-2.0.17/azure_cli_role.egg-info/requires.txt 2018-01-12 18:25:50.000000000 +0100 +++ new/azure-cli-role-2.0.22/azure_cli_role.egg-info/requires.txt 2018-04-06 19:33:56.000000000 +0200 @@ -1,5 +1,6 @@ azure-cli-core -azure-mgmt-authorization==0.30.0 -azure-graphrbac==0.31.0 +azure-mgmt-authorization==0.40.0 +azure-mgmt-monitor==0.5.0 +azure-graphrbac==0.40.0 azure-keyvault==0.3.7 pytz diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure-cli-role-2.0.17/setup.py new/azure-cli-role-2.0.22/setup.py --- old/azure-cli-role-2.0.17/setup.py 2018-01-12 18:25:22.000000000 +0100 +++ new/azure-cli-role-2.0.22/setup.py 2018-04-06 19:33:14.000000000 +0200 @@ -14,7 +14,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") cmdclass = {} -VERSION = "2.0.17" +VERSION = "2.0.22" CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -31,8 +31,9 @@ DEPENDENCIES = [ 'azure-cli-core', - 'azure-mgmt-authorization==0.30.0', - 'azure-graphrbac==0.31.0', + 'azure-mgmt-authorization==0.40.0', + 'azure-mgmt-monitor==0.5.0', + 'azure-graphrbac==0.40.0', 'azure-keyvault==0.3.7', 'pytz' ]