Skip to content

Commit

Permalink
Commands for managing users in an org (#484)
Browse files Browse the repository at this point in the history
* Add user list command

* Show and remove user commands

* User add and update command

* Changes in add command and adding send-email-invite argument

* Add a UT for user list

* Some more UTs

* Styling fixes

* Minor

* Pylint fix

* Renaming access-level to license-type and resolcing PR comments

* Minor table formatting

* Pylint fix
  • Loading branch information
ishitam8 committed Mar 18, 2019
1 parent 9f2b710 commit 90a4c92
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 0 deletions.
23 changes: 23 additions & 0 deletions azure-devops/azext_devops/dev/team/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,29 @@ def _transform_team_member_row(row):
return table_row


def transform_users_table_output(result):
table_output = []
for item in result:
table_output.append(_transform_user_row(item))
return table_output


def transform_user_table_output(result):
table_output = [_transform_user_row(result)]
return table_output


def _transform_user_row(row):
table_row = OrderedDict()
table_row['ID'] = row['id']
table_row['Display Name'] = row['user']['displayName']
table_row['Email'] = row['user']['mailAddress']
table_row['License Type'] = row['accessLevel']['accountLicenseType']
table_row['Access Level'] = row['accessLevel']['licenseDisplayName']
table_row['Status'] = row['accessLevel']['status']
return table_row


def _get_extension_key(extension):
return extension['extensionName'].lower()

Expand Down
5 changes: 5 additions & 0 deletions azure-devops/azext_devops/dev/team/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ def load_team_help():
short-summary: Manage teams
"""

helps['devops user'] = """
type: group
short-summary: Manage users
"""

helps['devops extension'] = """
type: group
short-summary: Manage extensions
Expand Down
9 changes: 9 additions & 0 deletions azure-devops/azext_devops/dev/team/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
_SERVICE_ENDPOINT_TYPE = [SERVICE_ENDPOINT_TYPE_GITHUB, SERVICE_ENDPOINT_TYPE_AZURE_RM]
_SERVICE_ENDPOINT_AUTHORIZATION_SCHEME = [SERVICE_ENDPOINT_AUTHORIZATION_PERSONAL_ACCESS_TOKEN,
SERVICE_ENDPOINT_AUTHORIZATION_SERVICE_PRINCIPAL]
_LICENSE_TYPES = ['advanced', 'earlyAdopter', 'express', 'none', 'professional', 'stakeholder']


def load_global_args(context):
Expand Down Expand Up @@ -51,6 +52,14 @@ def load_team_arguments(self, _):
context.argument('use_git_aliases', **enum_choice_list(_YES_NO_SWITCH_VALUES))
context.argument('list_config', options_list=('--list', '-l'))

with self.argument_context('devops user') as context:
from azure.cli.core.commands.parameters import get_enum_type
context.argument('license_type', arg_type=get_enum_type(_LICENSE_TYPES))
with self.argument_context('devops user add') as context:
from azure.cli.core.commands.parameters import get_enum_type
context.argument('send_email_invite', arg_type=get_enum_type(_TRUE_FALSE_SWITCH),
help='Whether to send email invite for new user or not.')

with self.argument_context('devops extension') as context:
from azure.cli.core.commands.parameters import get_enum_type
context.argument('include_built_in', arg_type=get_enum_type(_TRUE_FALSE_SWITCH),
Expand Down
14 changes: 14 additions & 0 deletions azure-devops/azext_devops/dev/team/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
transform_team_table_output,
transform_teams_table_output,
transform_team_members_table_output,
transform_users_table_output,
transform_user_table_output,
transform_extension_table_output,
transform_extensions_table_output)

Expand Down Expand Up @@ -44,6 +46,11 @@
exception_handler=azure_devops_exception_handler
)

userOps = CliCommandType(
operations_tmpl='azext_devops.dev.team.user#{}',
exception_handler=azure_devops_exception_handler
)

extensionOps = CliCommandType(
operations_tmpl='azext_devops.dev.team.extension#{}',
exception_handler=azure_devops_exception_handler
Expand Down Expand Up @@ -80,6 +87,13 @@ def load_team_commands(self, _):
g.command('list-member', 'get_team_members', table_transformer=transform_team_members_table_output)
g.command('update', 'update_team', table_transformer=transform_team_table_output)

with self.command_group('devops user', command_type=userOps) as g:
g.command('list', 'get_user_entitlements', table_transformer=transform_users_table_output)
g.command('show', 'get_user_entitlement', table_transformer=transform_user_table_output)
g.command('remove', 'delete_user_entitlement', confirmation='Are you sure you want to remove this user?')
g.command('update', 'update_user_entitlement', table_transformer=transform_user_table_output)
g.command('add', 'add_user_entitlement', table_transformer=transform_user_table_output)

with self.command_group('devops extension', command_type=extensionOps) as g:
g.command('list', 'list_extensions', table_transformer=transform_extensions_table_output)
g.command('uninstall', 'uninstall_extension', confirmation='Are you sure you want to uninstall this extension?')
Expand Down
108 changes: 108 additions & 0 deletions azure-devops/azext_devops/dev/team/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azext_devops.vstsCompressed.member_entitlement_management.v4_1.models.models import (AccessLevel,
GraphUser,
JsonPatchOperation)
from azext_devops.dev.common.services import (get_member_entitlement_management_client,
resolve_instance)
from azext_devops.dev.common.arguments import resolve_true_false
from azext_devops.dev.common.identities import resolve_identity_as_id


def get_user_entitlements(top=100, skip=None, organization=None, detect=None):
"""List users for an organization.
:param int top: Maximum number of the users to return. Max value is 10000.
:param int skip: Offset: Number of records to skip.
:rtype: [UserEntitlement]
"""
organization = resolve_instance(detect=detect, organization=organization)
client = get_member_entitlement_management_client(organization)
user_entitlements = client.get_user_entitlements(top=top, skip=skip)
return user_entitlements


def get_user_entitlement(user, organization=None, detect=None):
"""Show user details.
:param user: The Email id or UUID of the user.
:type user: str
:rtype: UserEntitlement
"""
organization = resolve_instance(detect=detect, organization=organization)
if '@' in user:
user = resolve_identity_as_id(user, organization)
client = get_member_entitlement_management_client(organization)
user_entitlement_details = client.get_user_entitlement(user_id=user)
return user_entitlement_details


def delete_user_entitlement(user, organization=None, detect=None):
"""Remove user from an organization.
:param user: The Email id or UUID of the user.
:type user: str
"""
organization = resolve_instance(detect=detect, organization=organization)
if '@' in user:
user = resolve_identity_as_id(user, organization)
client = get_member_entitlement_management_client(organization)
delete_user_entitlement_details = client.delete_user_entitlement(user_id=user)
return delete_user_entitlement_details


def update_user_entitlement(user, license_type, organization=None, detect=None):
"""Update license type for a user.
:param user: The Email id or UUID of the user.
:type user: str
:param license_type: License type for the user.
:type license_type: str
:rtype: UserEntitlementsPatchResponse
"""
patch_document = []
value = {}
value['accountLicenseType'] = license_type
patch_document.append(_create_patch_operation('replace', '/accessLevel', value))
organization = resolve_instance(detect=detect, organization=organization)
if '@' in user:
user = resolve_identity_as_id(user, organization)
client = get_member_entitlement_management_client(organization)
user_entitlement_update = client.update_user_entitlement(document=patch_document, user_id=user)
return user_entitlement_update.user_entitlement


def add_user_entitlement(user, license_type, send_email_invite='true', organization=None, detect=None):
"""Add user.
:param user: The Email id of the user.
:type user: str
:param license_type: License type for the user.
:type license_type: str
:rtype: UserEntitlementsPatchResponse
"""
do_not_send_invite = False
do_not_send_invite = not resolve_true_false(send_email_invite)
organization = resolve_instance(detect=detect, organization=organization)
client = get_member_entitlement_management_client(organization)
user_access_level = AccessLevel()
user_access_level.account_license_type = license_type
graph_user = GraphUser()
graph_user.subject_kind = 'user'
graph_user.principal_name = user
value = {}
value['accessLevel'] = user_access_level
value['extensions'] = []
value['projectEntitlements'] = []
value['user'] = graph_user
patch_document = []
patch_document.append(_create_patch_operation('add', '', value))
user_entitlement_details = client.update_user_entitlements(document=patch_document,
do_not_send_invite_for_new_users=do_not_send_invite)
return user_entitlement_details.results[0].result


def _create_patch_operation(op, path, value):
patch_operation = JsonPatchOperation()
patch_operation.op = op
patch_operation.path = path
patch_operation.value = value
return patch_operation
103 changes: 103 additions & 0 deletions azure-devops/azext_devops/test/team/test_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import unittest

try:
# Attempt to load mock (works on Python 3.3 and above)
from unittest.mock import patch
except ImportError:
# Attempt to load mock (works on Python version below 3.3)
from mock import patch

from azext_devops.vstsCompressed.member_entitlement_management.v4_1.member_entitlement_management_client import (MemberEntitlementManagementClient)
from azext_devops.dev.team.user import (get_user_entitlements,
get_user_entitlement,
add_user_entitlement,
delete_user_entitlement,
update_user_entitlement)

from azext_devops.dev.common.services import clear_connection_cache


class TestUserMethods(unittest.TestCase):

_TEST_DEVOPS_ORGANIZATION = 'https://someorganization.visualstudio.com'
_TEST_PROJECT_NAME = 'sample_project'
_TOP_VALUE = 10
_SKIP_VALUE = 2
_OFF = 'Off'
_TEST_USER_ID = 'adda517c-0398-42dc-b2a8-0d3f240757f9'
_USER_MGMT_CLIENT_LOCATION = 'azext_devops.vstsCompressed.member_entitlement_management.v4_1.member_entitlement_management_client.MemberEntitlementManagementClient.'

def setUp(self):
self.get_client = patch('azext_devops.vstsCompressed.vss_connection.VssConnection.get_client')
self.get_credential_patcher = patch('azext_devops.dev.common.services.get_credential')
self.get_patch_op_patcher = patch('azext_devops.dev.team.user._create_patch_operation')
self.list_user_patcher = patch(self._USER_MGMT_CLIENT_LOCATION + 'get_user_entitlements')
self.get_user_patcher = patch(self._USER_MGMT_CLIENT_LOCATION + 'get_user_entitlement')
self.add_user_patcher = patch(self._USER_MGMT_CLIENT_LOCATION + 'update_user_entitlements')
self.remove_user_patcher = patch(self._USER_MGMT_CLIENT_LOCATION + 'delete_user_entitlement')
self.update_user_patcher = patch(self._USER_MGMT_CLIENT_LOCATION + 'update_user_entitlement')

self.mock_get_client = self.get_client.start()
self.mock_get_users = self.list_user_patcher.start()
self.mock_add_user = self.add_user_patcher.start()
self.mock_get_user = self.get_user_patcher.start()
self.mock_remove_user = self.remove_user_patcher.start()
self.mock_update_user = self.update_user_patcher.start()
self.mock_get_credential = self.get_credential_patcher.start()

#set return values
self.mock_get_client.return_value = MemberEntitlementManagementClient(base_url=self._TEST_DEVOPS_ORGANIZATION)

#clear connection cache before running each test
clear_connection_cache()

def tearDown(self):
patch.stopall()

def test_list_user(self):
get_user_entitlements(top=self._TOP_VALUE, skip=self._SKIP_VALUE,
organization=self._TEST_DEVOPS_ORGANIZATION, detect=self._OFF)
#assert
self.mock_get_users.assert_called_once()
list_user_param = self.mock_get_users.call_args_list[0][1]
self.assertEqual(10,list_user_param['top'])
self.assertEqual(2, list_user_param['skip'])

def test_show_user(self):
get_user_entitlement(user=self._TEST_USER_ID, organization=self._TEST_DEVOPS_ORGANIZATION, detect=self._OFF)
#assert
self.mock_get_user.assert_called_once_with(user_id = 'adda517c-0398-42dc-b2a8-0d3f240757f9')

def test_add_user(self):
add_user_entitlement(user='someuser@xyz.com', license_type='stakeholder',
organization=self._TEST_DEVOPS_ORGANIZATION, detect=self._OFF)
#assert
self.mock_add_user.assert_called_once()
add_user_param = self.mock_add_user.call_args_list[0][1]
add_user_param_document = add_user_param['document'][0].value
self.assertEqual(False, add_user_param['do_not_send_invite_for_new_users'])
self.assertEqual('stakeholder', add_user_param_document['accessLevel'].account_license_type)
self.assertEqual('user', add_user_param_document['user'].subject_kind)
self.assertEqual('someuser@xyz.com', add_user_param_document['user'].principal_name)

def test_remove_user(self):
delete_user_entitlement(user=self._TEST_USER_ID, organization= self._TEST_DEVOPS_ORGANIZATION, detect=self._OFF)
#assert
self.mock_remove_user.assert_called_once()

def test_update_user(self):
update_user_entitlement(user=self._TEST_USER_ID, license_type='express',
organization=self._TEST_DEVOPS_ORGANIZATION, detect=self._OFF)
#assert
self.mock_update_user.assert_called_once()
update_user_param = self.mock_update_user.call_args_list[0][1]
update_user_param_document = update_user_param['document'][0].value
print(update_user_param_document)
self.assertEqual('express', update_user_param_document['accountLicenseType'])
self.assertEqual('adda517c-0398-42dc-b2a8-0d3f240757f9', update_user_param['user_id'])

0 comments on commit 90a4c92

Please sign in to comment.