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

Commands for managing users in an org #484

Merged
merged 14 commits into from
Mar 18, 2019
29 changes: 29 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,35 @@ 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']
if 'user' in row:
table_row['Display Name'] = row['user']['displayName']
table_row['Email'] = row['user']['mailAddress']
else:
table_row['Display Name'] = ' '
ishitam8 marked this conversation as resolved.
Show resolved Hide resolved
if 'accessLevel' in row:
table_row['Access Level'] = row['accessLevel']['licenseDisplayName']
table_row['Status'] = row['accessLevel']['status']
else:
table_row['Access Level'] = ' '
table_row['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
8 changes: 8 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]
_ACCESS_LEVELS = ['advanced', 'earlyAdopter', 'express', 'none', 'professional', 'stakeholder']
ishitam8 marked this conversation as resolved.
Show resolved Hide resolved
atbagga marked this conversation as resolved.
Show resolved Hide resolved


def load_global_args(context):
Expand Down Expand Up @@ -51,6 +52,13 @@ 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:
context.argument('access_level', **enum_choice_list(_ACCESS_LEVELS))
ishitam8 marked this conversation as resolved.
Show resolved Hide resolved
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.identities import resolve_identity_as_id


def get_user_entitlements(top=None, 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. Default value is 100
ishitam8 marked this conversation as resolved.
Show resolved Hide resolved
: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, access_level, organization=None, detect=None):
"""Update access level for a user.
:param user: The Email id or UUID of the user.
:type user: str
:param access_level: Access level for the user.
:type access_level: str
:rtype: UserEntitlementsPatchResponse
"""
patch_document = []
value = {}
value['accountLicenseType'] = access_level
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, access_level, send_email_invite='true', organization=None, detect=None):
"""Add user.
:param user: The Email id of the user.
:type user: str
:param access_level: Access level for the user.
:type access_level: str
:rtype: UserEntitlementsPatchResponse
"""
organization = resolve_instance(detect=detect, organization=organization)
client = get_member_entitlement_management_client(organization)
user_access_level = AccessLevel()
user_access_level.account_license_type = access_level
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))
do_not_send_invite = False
if send_email_invite != 'true':
do_not_send_invite = True
ishitam8 marked this conversation as resolved.
Show resolved Hide resolved
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):
ishitam8 marked this conversation as resolved.
Show resolved Hide resolved

_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='[email protected]', access_level='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('[email protected]', 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, access_level='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'])