Skip to content

Commit 6e49d47

Browse files
authored
Feature/add bulk user role commands (#331)
* bulk user role commands * bulk user roles * bulk cmds * fixing unit tests * renaming variable * add cache decorate to _get_role_id() * changed to lru_cache for <3.9 compatability * fix
1 parent 66c457b commit 6e49d47

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1616

1717
### Added
1818

19+
- New bulk commands to manage user roles
20+
- `code42 users bulk add-roles`
21+
- `code42 users bulk remove-roles`
22+
1923
- New option `--include-roles` on `code42 users list` that includes the roles for all users.
2024

2125
- New command `code42 users show <username>` that prints all the details of that user.

src/code42cli/cmds/users.py

+85
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import functools
2+
13
import click
24
from pandas import DataFrame
35
from pandas import json_normalize
@@ -217,6 +219,8 @@ def reactivate(state, username):
217219

218220
_bulk_user_move_headers = ["username", "org_id"]
219221

222+
_bulk_user_roles_headers = ["username", "role_name"]
223+
220224

221225
@users.command(name="move")
222226
@username_option("The username of the user to move.", required=True)
@@ -461,6 +465,86 @@ def handle_row(**row):
461465
formatter.echo_formatted_list(result_rows)
462466

463467

468+
@bulk.command(
469+
name="add-roles",
470+
help=f"Add roles to a list of users from the provided CSV in format: {','.join(_bulk_user_roles_headers)}",
471+
)
472+
@read_csv_arg(headers=_bulk_user_roles_headers)
473+
@format_option
474+
@sdk_options()
475+
def bulk_add_roles(state, csv_rows, format):
476+
"""Bulk add roles to a list of users."""
477+
478+
# Initialize the SDK before starting any bulk processes
479+
# to prevent multiple instances and having to enter 2fa multiple times.
480+
sdk = state.sdk
481+
status_header = "role added"
482+
483+
csv_rows[0][status_header] = "False"
484+
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})
485+
stats = create_worker_stats(len(csv_rows))
486+
487+
def handle_row(**row):
488+
try:
489+
_add_user_role(
490+
sdk, **{key: row[key] for key in row.keys() if key != status_header}
491+
)
492+
row[status_header] = "True"
493+
except Exception as err:
494+
row[status_header] = f"False: {err}"
495+
stats.increment_total_errors()
496+
return row
497+
498+
result_rows = run_bulk_process(
499+
handle_row,
500+
csv_rows,
501+
progress_label="Adding roles to users:",
502+
stats=stats,
503+
raise_global_error=False,
504+
)
505+
formatter.echo_formatted_list(result_rows)
506+
507+
508+
@bulk.command(
509+
name="remove-roles",
510+
help=f"Remove roles from a list of users from the provided CSV in format: {','.join(_bulk_user_roles_headers)}",
511+
)
512+
@read_csv_arg(headers=_bulk_user_roles_headers)
513+
@format_option
514+
@sdk_options()
515+
def bulk_remove_roles(state, csv_rows, format):
516+
"""Bulk remove roles from a list of users."""
517+
518+
# Initialize the SDK before starting any bulk processes
519+
# to prevent multiple instances and having to enter 2fa multiple times.
520+
sdk = state.sdk
521+
success_header = "role removed"
522+
523+
csv_rows[0][success_header] = "False"
524+
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})
525+
stats = create_worker_stats(len(csv_rows))
526+
527+
def handle_row(**row):
528+
try:
529+
_remove_user_role(
530+
sdk, **{key: row[key] for key in row.keys() if key != success_header}
531+
)
532+
row[success_header] = "True"
533+
except Exception as err:
534+
row[success_header] = f"False: {err}"
535+
stats.increment_total_errors()
536+
return row
537+
538+
result_rows = run_bulk_process(
539+
handle_row,
540+
csv_rows,
541+
progress_label="Removing roles from users:",
542+
stats=stats,
543+
raise_global_error=False,
544+
)
545+
formatter.echo_formatted_list(result_rows)
546+
547+
464548
def _add_user_role(sdk, username, role_name):
465549
user_id = _get_legacy_user_id(sdk, username)
466550
_get_role_id(sdk, role_name) # function provides role name validation
@@ -484,6 +568,7 @@ def _get_legacy_user_id(sdk, username):
484568
return user_id
485569

486570

571+
@functools.lru_cache()
487572
def _get_role_id(sdk, role_name):
488573
try:
489574
roles_dataframe = DataFrame.from_records(

tests/cmds/test_users.py

+148
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,154 @@ def _get(username, *args, **kwargs):
11081108
assert worker_stats.increment_total_errors.call_count == 1
11091109

11101110

1111+
def test_bulk_add_roles_uses_expected_arguments(runner, mocker, cli_state_with_user):
1112+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
1113+
with runner.isolated_filesystem():
1114+
with open("test_bulk_add_roles.csv", "w") as csv:
1115+
csv.writelines(
1116+
["username,role_name\n", f"{TEST_USERNAME},{TEST_ROLE_NAME}\n"]
1117+
)
1118+
command = ["users", "bulk", "add-roles", "test_bulk_add_roles.csv"]
1119+
runner.invoke(
1120+
cli, command, obj=cli_state_with_user,
1121+
)
1122+
assert bulk_processor.call_args[0][1] == [
1123+
{"username": TEST_USERNAME, "role_name": TEST_ROLE_NAME, "role added": "False"},
1124+
]
1125+
bulk_processor.assert_called_once()
1126+
1127+
1128+
def test_bulk_add_roles_ignores_blank_lines(runner, mocker, cli_state):
1129+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
1130+
with runner.isolated_filesystem():
1131+
with open("test_bulk_add_roles.csv", "w") as csv:
1132+
csv.writelines(
1133+
["username,role_name\n\n\n", f"{TEST_USERNAME},{TEST_ROLE_NAME}\n\n\n"]
1134+
)
1135+
runner.invoke(
1136+
cli,
1137+
["users", "bulk", "add-roles", "test_bulk_add_roles.csv"],
1138+
obj=cli_state,
1139+
)
1140+
assert bulk_processor.call_args[0][1] == [
1141+
{"username": TEST_USERNAME, "role_name": TEST_ROLE_NAME, "role added": "False"},
1142+
]
1143+
bulk_processor.assert_called_once()
1144+
1145+
1146+
def test_bulk_add_roles_uses_handler_that_when_encounters_error_increments_total_errors(
1147+
runner,
1148+
mocker,
1149+
cli_state,
1150+
worker_stats,
1151+
get_users_response,
1152+
get_available_roles_success,
1153+
):
1154+
def _get(username, *args, **kwargs):
1155+
if username == "[email protected]":
1156+
raise Exception("TEST")
1157+
return get_users_response
1158+
1159+
cli_state.sdk.users.get_by_username.side_effect = _get
1160+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
1161+
with runner.isolated_filesystem():
1162+
with open("test_bulk_add_roles.csv", "w") as csv:
1163+
csv.writelines(
1164+
["username,role_name\n", f"{TEST_USERNAME},{TEST_ROLE_NAME}\n"]
1165+
)
1166+
1167+
runner.invoke(
1168+
cli,
1169+
["users", "bulk", "add-roles", "test_bulk_add_roles.csv"],
1170+
obj=cli_state,
1171+
)
1172+
handler = bulk_processor.call_args[0][0]
1173+
1174+
handler(
1175+
username="[email protected]", role_name=TEST_ROLE_NAME,
1176+
)
1177+
handler(username="[email protected]", role_name=TEST_ROLE_NAME)
1178+
assert worker_stats.increment_total_errors.call_count == 1
1179+
1180+
1181+
def test_bulk_remove_roles_uses_expected_arguments(runner, mocker, cli_state_with_user):
1182+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
1183+
with runner.isolated_filesystem():
1184+
with open("test_bulk_remove_roles.csv", "w") as csv:
1185+
csv.writelines(
1186+
["username,role_name\n", f"{TEST_USERNAME},{TEST_ROLE_NAME}\n"]
1187+
)
1188+
command = ["users", "bulk", "remove-roles", "test_bulk_remove_roles.csv"]
1189+
runner.invoke(
1190+
cli, command, obj=cli_state_with_user,
1191+
)
1192+
assert bulk_processor.call_args[0][1] == [
1193+
{
1194+
"username": TEST_USERNAME,
1195+
"role_name": TEST_ROLE_NAME,
1196+
"role removed": "False",
1197+
},
1198+
]
1199+
bulk_processor.assert_called_once()
1200+
1201+
1202+
def test_bulk_remove_roles_ignores_blank_lines(runner, mocker, cli_state):
1203+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
1204+
with runner.isolated_filesystem():
1205+
with open("test_bulk_remove_roles.csv", "w") as csv:
1206+
csv.writelines(
1207+
["username,role_name\n\n\n", f"{TEST_USERNAME},{TEST_ROLE_NAME}\n\n\n"]
1208+
)
1209+
runner.invoke(
1210+
cli,
1211+
["users", "bulk", "remove-roles", "test_bulk_remove_roles.csv"],
1212+
obj=cli_state,
1213+
)
1214+
assert bulk_processor.call_args[0][1] == [
1215+
{
1216+
"username": TEST_USERNAME,
1217+
"role_name": TEST_ROLE_NAME,
1218+
"role removed": "False",
1219+
},
1220+
]
1221+
bulk_processor.assert_called_once()
1222+
1223+
1224+
def test_bulk_remove_roles_uses_handler_that_when_encounters_error_increments_total_errors(
1225+
runner,
1226+
mocker,
1227+
cli_state,
1228+
worker_stats,
1229+
get_users_response,
1230+
get_available_roles_success,
1231+
):
1232+
def _get(username, *args, **kwargs):
1233+
if username == "[email protected]":
1234+
raise Exception("TEST")
1235+
1236+
return get_users_response
1237+
1238+
cli_state.sdk.users.get_by_username.side_effect = _get
1239+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
1240+
with runner.isolated_filesystem():
1241+
with open("test_bulk_remove_roles.csv", "w") as csv:
1242+
csv.writelines(
1243+
["username,role_name\n", f"{TEST_USERNAME},{TEST_ROLE_NAME}\n"]
1244+
)
1245+
1246+
runner.invoke(
1247+
cli,
1248+
["users", "bulk", "remove-roles", "test_bulk_remove_roles.csv"],
1249+
obj=cli_state,
1250+
)
1251+
handler = bulk_processor.call_args[0][0]
1252+
handler(
1253+
username="[email protected]", role_name=TEST_ROLE_NAME,
1254+
)
1255+
handler(username="[email protected]", role_name=TEST_ROLE_NAME)
1256+
assert worker_stats.increment_total_errors.call_count == 1
1257+
1258+
11111259
def test_orgs_list_calls_orgs_get_all_with_expected_params(runner, cli_state):
11121260
runner.invoke(cli, ["users", "orgs", "list"], obj=cli_state)
11131261
assert cli_state.sdk.orgs.get_all.call_count == 1

0 commit comments

Comments
 (0)