diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0f8af7..2d18fa71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The intended audience of this file is for py42 consumers -- as such, changes that don't affect how a consumer would use the library (e.g. adding unit tests, updating documentation, etc) are not captured here. +## Unreleased + +### Added + +- New command `code42 users add-role` to add a user role to a single user. + +- New command `code42 users remove-role` to remove a user role from a single user. + ## 1.6.1 - 2021-05-27 ### Fixed diff --git a/src/code42cli/cmds/users.py b/src/code42cli/cmds/users.py index b1f48f9c..d368dcca 100644 --- a/src/code42cli/cmds/users.py +++ b/src/code42cli/cmds/users.py @@ -4,6 +4,7 @@ from code42cli.click_ext.groups import OrderedGroup from code42cli.click_ext.options import incompatible_with from code42cli.errors import Code42CLIError +from code42cli.errors import UserDoesNotExistError from code42cli.options import format_option from code42cli.options import sdk_options from code42cli.output_formats import DataFrameOutputFormatter @@ -21,9 +22,6 @@ def users(state): "--org-uid", help="Limit users to only those in the organization you specify. Note that child orgs are included.", ) -role_name_option = click.option( - "--role-name", help="Limit results to only users having the specified role.", -) active_option = click.option( "--active", is_flag=True, help="Limits results to only active users.", default=None, ) @@ -35,9 +33,17 @@ def users(state): ) +def role_name_option(help): + return click.option("--role-name", help=help) + + +def username_option(help): + return click.option("--username", help=help) + + @users.command(name="list") @org_uid_option -@role_name_option +@role_name_option("Limit results to only users having the specified role.") @active_option @inactive_option @format_option @@ -60,6 +66,44 @@ def list_users(state, org_uid, role_name, active, inactive, format): formatter.echo_formatted_dataframe(df) +@users.command() +@username_option("Username of the target user.") +@role_name_option("Name of role to add.") +@sdk_options() +def add_role(state, username, role_name): + """Add the specified role to the user with the specified username.""" + _add_user_role(state.sdk, username, role_name) + + +@users.command() +@role_name_option("Name of role to remove.") +@username_option("Username of the target user.") +@sdk_options() +def remove_role(state, username, role_name): + """Remove the specified role to the user with the specified username.""" + _remove_user_role(state.sdk, role_name, username) + + +def _add_user_role(sdk, username, role_name): + user_id = _get_user_id(sdk, username) + _get_role_id(sdk, role_name) # function provides role name validation + sdk.users.add_role(user_id, role_name) + + +def _remove_user_role(sdk, role_name, username): + user_id = _get_user_id(sdk, username) + _get_role_id(sdk, role_name) # function provides role name validation + sdk.users.remove_role(user_id, role_name) + + +def _get_user_id(sdk, username): + user = sdk.users.get_by_username(username)["users"] + if len(user) == 0: + raise UserDoesNotExistError(username) + user_id = user[0]["userId"] + return user_id + + def _get_role_id(sdk, role_name): try: roles_dataframe = DataFrame.from_records( @@ -68,7 +112,7 @@ def _get_role_id(sdk, role_name): role_result = roles_dataframe.at[role_name, "roleId"] return str(role_result) # extract the role ID from the series except KeyError: - raise Code42CLIError(f"Role with name {role_name} not found.") + raise Code42CLIError(f"Role with name '{role_name}' not found.") def _get_users_dataframe(sdk, columns, org_uid, role_id, active): diff --git a/tests/cmds/test_users.py b/tests/cmds/test_users.py index 634575cd..506fbde7 100644 --- a/tests/cmds/test_users.py +++ b/tests/cmds/test_users.py @@ -32,6 +32,10 @@ } ] } +TEST_EMPTY_USERS_RESPONSE = {"users": []} +TEST_USERNAME = TEST_USERS_RESPONSE["users"][0]["username"] +TEST_USER_ID = TEST_USERS_RESPONSE["users"][0]["userId"] +TEST_ROLE_NAME = TEST_ROLE_RETURN_DATA["data"][0]["roleName"] def _create_py42_response(mocker, text): @@ -56,6 +60,16 @@ def get_all_users_success(cli_state): cli_state.sdk.users.get_all.return_value = get_all_users_generator() +@pytest.fixture +def get_user_id_success(cli_state): + cli_state.sdk.users.get_by_username.return_value = TEST_USERS_RESPONSE + + +@pytest.fixture +def get_user_id_failure(cli_state): + cli_state.sdk.users.get_by_username.return_value = TEST_EMPTY_USERS_RESPONSE + + @pytest.fixture def get_available_roles_success(cli_state, get_available_roles_response): cli_state.sdk.users.get_available_roles.return_value = get_available_roles_response @@ -153,3 +167,99 @@ def test_list_users_when_given_excluding_active_and_inactive_uses_active_equals_ cli_state.sdk.users.get_all.assert_called_once_with( active=None, org_uid=None, role_id=None ) + + +def test_add_user_role_adds( + runner, cli_state, get_user_id_success, get_available_roles_success +): + command = [ + "users", + "add-role", + "--username", + "test.username@example.com", + "--role-name", + "Customer Cloud Admin", + ] + runner.invoke(cli, command, obj=cli_state) + cli_state.sdk.users.add_role.assert_called_once_with(TEST_USER_ID, TEST_ROLE_NAME) + + +def test_add_user_role_raises_error_when_role_does_not_exist( + runner, cli_state, get_user_id_success, get_available_roles_success +): + command = [ + "users", + "add-role", + "--username", + "test.username@example.com", + "--role-name", + "test", + ] + result = runner.invoke(cli, command, obj=cli_state) + assert result.exit_code == 1 + assert "Role with name 'test' not found." in result.output + + +def test_add_user_role_raises_error_when_username_does_not_exist( + runner, cli_state, get_user_id_failure, get_available_roles_success +): + command = [ + "users", + "add-role", + "--username", + "not_a_username@example.com", + "--role-name", + "Desktop User", + ] + result = runner.invoke(cli, command, obj=cli_state) + assert result.exit_code == 1 + assert "User 'not_a_username@example.com' does not exist." in result.output + + +def test_remove_user_role_removes( + runner, cli_state, get_user_id_success, get_available_roles_success +): + command = [ + "users", + "remove-role", + "--username", + "test.username@example.com", + "--role-name", + "Customer Cloud Admin", + ] + runner.invoke(cli, command, obj=cli_state) + cli_state.sdk.users.remove_role.assert_called_once_with( + TEST_USER_ID, TEST_ROLE_NAME + ) + + +def test_remove_user_role_raises_error_when_role_does_not_exist( + runner, cli_state, get_user_id_success, get_available_roles_success +): + command = [ + "users", + "remove-role", + "--username", + "test.username@example.com", + "--role-name", + "test", + ] + result = runner.invoke(cli, command, obj=cli_state) + assert result.exit_code == 1 + assert "Role with name 'test' not found." in result.output + + +def test_remove_user_role_raises_error_when_username_does_not_exist( + runner, cli_state, get_user_id_failure, get_available_roles_success +): + command = [ + "users", + "remove-role", + "--username", + "not_a_username@example.com", + "--role-name", + "Desktop User", + ] + result = runner.invoke(cli, command, obj=cli_state) + assert result.exit_code == 1 + assert "User 'not_a_username@example.com' does not exist." in result.output