Skip to content

Commit fe448b1

Browse files
Juliya Smithtimabrmsn
andauthored
users deactivate and reactivate commands (#317)
* users deactivate and reactivate commands * d>r Co-authored-by: tim.abramson <tim.abramson@code42.com>
1 parent 02a9224 commit fe448b1

4 files changed

Lines changed: 289 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
The intended audience of this file is for py42 consumers -- as such, changes that don't affect
99
how a consumer would use the library (e.g. adding unit tests, updating documentation, etc) are not captured here.
1010

11+
## Unreleased
12+
13+
### Added
14+
15+
- New commands:
16+
- `code42 users deactivate`
17+
- `code42 users reactivate`
18+
- `code42 users bulk deactivate`
19+
- `code42 users bulk reactivate`
20+
1121
## 1.9.0 - 2021-08-19
1222

1323
### Added

src/code42cli/click_ext/groups.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from collections import OrderedDict
44

55
import click
6+
from py42.exceptions import Py42ActiveLegalHoldError
67
from py42.exceptions import Py42CaseAlreadyHasEventError
78
from py42.exceptions import Py42CaseNameExistsError
89
from py42.exceptions import Py42DescriptionLimitExceededError
@@ -75,6 +76,7 @@ def invoke(self, ctx):
7576
Py42InvalidEmailError,
7677
Py42InvalidPasswordError,
7778
Py42InvalidUsernameError,
79+
Py42ActiveLegalHoldError,
7880
) as err:
7981
self.logger.log_error(err)
8082
raise Code42CLIError(str(err))

src/code42cli/cmds/users.py

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,9 @@ def users(state):
3636
help="Limits results to only deactivated users.",
3737
cls=incompatible_with("active"),
3838
)
39-
40-
41-
user_uid_option = click.option(
39+
user_id_option = click.option(
4240
"--user-id", help="The unique identifier of the user to be modified.", required=True
4341
)
44-
4542
org_id_option = click.option(
4643
"--org-id",
4744
help="The identifier for the organization to which the user will be moved.",
@@ -101,7 +98,7 @@ def remove_role(state, username, role_name):
10198

10299

103100
@users.command(name="update")
104-
@user_uid_option
101+
@user_id_option
105102
@click.option("--username", help="The new username for the user.")
106103
@click.option("--password", help="The new password for the user.")
107104
@click.option("--email", help="The new email for the user.")
@@ -137,6 +134,24 @@ def update_user(
137134
)
138135

139136

137+
@users.command()
138+
@click.argument("username")
139+
@sdk_options()
140+
def deactivate(state, username):
141+
"""Deactivate a user."""
142+
sdk = state.sdk
143+
_deactivate_user(sdk, username)
144+
145+
146+
@users.command()
147+
@click.argument("username")
148+
@sdk_options()
149+
def reactivate(state, username):
150+
"""Reactivate a user."""
151+
sdk = state.sdk
152+
_reactivate_user(sdk, username)
153+
154+
140155
_bulk_user_update_headers = [
141156
"user_id",
142157
"username",
@@ -259,19 +274,100 @@ def handle_row(**row):
259274
formatter.echo_formatted_list(result_rows)
260275

261276

277+
_bulk_user_activation_headers = ["username"]
278+
279+
280+
@bulk.command(
281+
name="deactivate",
282+
help=f"Deactivate a list of users from the provided CSV in format: {','.join(_bulk_user_activation_headers)}",
283+
)
284+
@read_csv_arg(headers=_bulk_user_activation_headers)
285+
@format_option
286+
@sdk_options()
287+
def bulk_deactivate(state, csv_rows, format):
288+
"""Deactivate a list of users."""
289+
290+
# Initialize the SDK before starting any bulk processes
291+
# to prevent multiple instances and having to enter 2fa multiple times.
292+
sdk = state.sdk
293+
294+
csv_rows[0]["deactivated"] = "False"
295+
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})
296+
stats = create_worker_stats(len(csv_rows))
297+
298+
def handle_row(**row):
299+
try:
300+
_deactivate_user(
301+
sdk, **{key: row[key] for key in row.keys() if key != "deactivated"}
302+
)
303+
row["deactivated"] = "True"
304+
except Exception as err:
305+
row["deactivated"] = f"False: {err}"
306+
stats.increment_total_errors()
307+
return row
308+
309+
result_rows = run_bulk_process(
310+
handle_row,
311+
csv_rows,
312+
progress_label="Deactivating users:",
313+
stats=stats,
314+
raise_global_error=False,
315+
)
316+
formatter.echo_formatted_list(result_rows)
317+
318+
319+
@bulk.command(
320+
name="reactivate",
321+
help=f"Reactivate a list of users from the provided CSV in format: {','.join(_bulk_user_activation_headers)}",
322+
)
323+
@read_csv_arg(headers=_bulk_user_activation_headers)
324+
@format_option
325+
@sdk_options()
326+
def bulk_reactivate(state, csv_rows, format):
327+
"""Reactivate a list of users."""
328+
329+
# Initialize the SDK before starting any bulk processes
330+
# to prevent multiple instances and having to enter 2fa multiple times.
331+
sdk = state.sdk
332+
333+
csv_rows[0]["reactivated"] = "False"
334+
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})
335+
stats = create_worker_stats(len(csv_rows))
336+
337+
def handle_row(**row):
338+
try:
339+
_reactivate_user(
340+
sdk, **{key: row[key] for key in row.keys() if key != "reactivated"}
341+
)
342+
row["reactivated"] = "True"
343+
except Exception as err:
344+
row["reactivated"] = f"False: {err}"
345+
stats.increment_total_errors()
346+
return row
347+
348+
result_rows = run_bulk_process(
349+
handle_row,
350+
csv_rows,
351+
progress_label="Reactivating users:",
352+
stats=stats,
353+
raise_global_error=False,
354+
)
355+
formatter.echo_formatted_list(result_rows)
356+
357+
262358
def _add_user_role(sdk, username, role_name):
263-
user_id = _get_user_id(sdk, username)
359+
user_id = _get_legacy_user_id(sdk, username)
264360
_get_role_id(sdk, role_name) # function provides role name validation
265361
sdk.users.add_role(user_id, role_name)
266362

267363

268364
def _remove_user_role(sdk, role_name, username):
269-
user_id = _get_user_id(sdk, username)
365+
user_id = _get_legacy_user_id(sdk, username)
270366
_get_role_id(sdk, role_name) # function provides role name validation
271367
sdk.users.remove_role(user_id, role_name)
272368

273369

274-
def _get_user_id(sdk, username):
370+
def _get_legacy_user_id(sdk, username):
275371
if not username:
276372
# py42 returns all users when passing `None` to `get_by_username()`.
277373
raise click.BadParameter("Username is required.")
@@ -326,11 +422,21 @@ def _update_user(
326422

327423

328424
def _change_organization(sdk, username, org_id):
329-
user_id = _get_user_id(sdk, username)
425+
user_id = _get_legacy_user_id(sdk, username)
330426
org_id = _get_org_id(sdk, org_id)
331427
return sdk.users.change_org_assignment(user_id=int(user_id), org_id=int(org_id))
332428

333429

334430
def _get_org_id(sdk, org_id):
335431
org = sdk.orgs.get_by_uid(org_id)
336432
return org["orgId"]
433+
434+
435+
def _deactivate_user(sdk, username):
436+
user_id = _get_legacy_user_id(sdk, username)
437+
sdk.users.deactivate(user_id)
438+
439+
440+
def _reactivate_user(sdk, username):
441+
user_id = _get_legacy_user_id(sdk, username)
442+
sdk.users.reactivate(user_id)

tests/cmds/test_users.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from py42.exceptions import Py42ActiveLegalHoldError
23
from py42.exceptions import Py42InvalidEmailError
34
from py42.exceptions import Py42InvalidPasswordError
45
from py42.exceptions import Py42InvalidUsernameError
@@ -130,6 +131,23 @@ def update_user_success(cli_state, update_user_response):
130131
cli_state.sdk.users.update_user.return_value = update_user_response
131132

132133

134+
@pytest.fixture
135+
def deactivate_user_success(mocker, cli_state):
136+
cli_state.sdk.users.deactivate.return_value = create_mock_response(mocker)
137+
138+
139+
@pytest.fixture
140+
def deactivate_user_legal_hold_failure(mocker, cli_state):
141+
cli_state.sdk.users.deactivate.side_effect = Py42ActiveLegalHoldError(
142+
create_mock_http_error(mocker, status=400), "user", TEST_USER_ID
143+
)
144+
145+
146+
@pytest.fixture
147+
def reactivate_user_success(mocker, cli_state):
148+
cli_state.sdk.users.deactivate.return_value = create_mock_response(mocker)
149+
150+
133151
@pytest.fixture
134152
def change_org_success(cli_state, change_org_response):
135153
cli_state.sdk.users.change_org_assignment.return_value = change_org_response
@@ -433,6 +451,33 @@ def test_update_when_py42_raises_invalid_password_outputs_error_message(
433451
assert "Error: Invalid password." in result.output
434452

435453

454+
def test_deactivate_calls_deactivate_with_correct_parameters(
455+
runner, cli_state, get_user_id_success, deactivate_user_success
456+
):
457+
command = ["users", "deactivate", "test@example.com"]
458+
runner.invoke(cli, command, obj=cli_state)
459+
cli_state.sdk.users.deactivate.assert_called_once_with(TEST_USER_ID)
460+
461+
462+
def test_deactivate_when_user_on_legal_hold_outputs_expected_error_text(
463+
runner, cli_state, get_user_id_success, deactivate_user_legal_hold_failure
464+
):
465+
command = ["users", "deactivate", "test@example.com"]
466+
result = runner.invoke(cli, command, obj=cli_state)
467+
assert (
468+
"Error: Cannot deactivate the user with ID 1234 as the user is involved in a legal hold matter."
469+
in result.output
470+
)
471+
472+
473+
def test_reactivate_calls_reactivate_with_correct_parameters(
474+
runner, cli_state, get_user_id_success, deactivate_user_success
475+
):
476+
command = ["users", "reactivate", "test@example.com"]
477+
runner.invoke(cli, command, obj=cli_state)
478+
cli_state.sdk.users.reactivate.assert_called_once_with(TEST_USER_ID)
479+
480+
436481
def test_bulk_update_uses_expected_arguments_when_only_some_are_passed(
437482
runner, mocker, cli_state
438483
):
@@ -655,3 +700,120 @@ def test_bulk_move_uses_handle_than_when_called_and_row_has_missing_username_err
655700
assert worker_stats.increment_total_errors.call_count == 1
656701
# Ensure it does not try to get the username for the None user.
657702
assert not cli_state.sdk.users.get_by_username.call_count
703+
704+
705+
def test_bulk_deactivate_uses_expected_arguments(runner, mocker, cli_state):
706+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
707+
with runner.isolated_filesystem():
708+
with open("test_bulk_deactivate.csv", "w") as csv:
709+
csv.writelines(["username\n", f"{TEST_USERNAME}\n"])
710+
runner.invoke(
711+
cli,
712+
["users", "bulk", "deactivate", "test_bulk_deactivate.csv"],
713+
obj=cli_state,
714+
)
715+
assert bulk_processor.call_args[0][1] == [
716+
{"username": TEST_USERNAME, "deactivated": "False"}
717+
]
718+
bulk_processor.assert_called_once()
719+
720+
721+
def test_bulk_deactivate_ignores_blank_lines(runner, mocker, cli_state):
722+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
723+
with runner.isolated_filesystem():
724+
with open("test_bulk_deactivate.csv", "w") as csv:
725+
csv.writelines(["username\n\n\n", f"{TEST_USERNAME}\n\n\n"])
726+
runner.invoke(
727+
cli,
728+
["users", "bulk", "deactivate", "test_bulk_deactivate.csv"],
729+
obj=cli_state,
730+
)
731+
assert bulk_processor.call_args[0][1] == [
732+
{"username": TEST_USERNAME, "deactivated": "False"}
733+
]
734+
bulk_processor.assert_called_once()
735+
736+
737+
def test_bulk_deactivate_uses_handler_that_when_encounters_error_increments_total_errors(
738+
runner, mocker, cli_state, worker_stats, get_users_response
739+
):
740+
lines = ["username\n", f"{TEST_USERNAME}\n"]
741+
742+
def _get(username, *args, **kwargs):
743+
if username == "test@example.com":
744+
raise Exception("TEST")
745+
return get_users_response
746+
747+
cli_state.sdk.users.get_by_username.side_effect = _get
748+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
749+
with runner.isolated_filesystem():
750+
with open("test_bulk_deactivate.csv", "w") as csv:
751+
csv.writelines(lines)
752+
runner.invoke(
753+
cli,
754+
["users", "bulk", "deactivate", "test_bulk_deactivate.csv"],
755+
obj=cli_state,
756+
)
757+
handler = bulk_processor.call_args[0][0]
758+
handler(username="test@example.com")
759+
handler(username="not.test@example.com")
760+
assert worker_stats.increment_total_errors.call_count == 1
761+
762+
763+
def test_bulk_reactivate_uses_expected_arguments(runner, mocker, cli_state):
764+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
765+
with runner.isolated_filesystem():
766+
with open("test_bulk_reactivate.csv", "w") as csv:
767+
csv.writelines(["username\n", f"{TEST_USERNAME}\n"])
768+
runner.invoke(
769+
cli,
770+
["users", "bulk", "reactivate", "test_bulk_reactivate.csv"],
771+
obj=cli_state,
772+
)
773+
assert bulk_processor.call_args[0][1] == [
774+
{"username": TEST_USERNAME, "reactivated": "False"}
775+
]
776+
bulk_processor.assert_called_once()
777+
778+
779+
def test_bulk_reactivate_ignores_blank_lines(runner, mocker, cli_state):
780+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
781+
with runner.isolated_filesystem():
782+
with open("test_bulk_reactivate.csv", "w") as csv:
783+
csv.writelines(["username\n\n\n", f"{TEST_USERNAME}\n\n\n"])
784+
runner.invoke(
785+
cli,
786+
["users", "bulk", "reactivate", "test_bulk_reactivate.csv"],
787+
obj=cli_state,
788+
)
789+
assert bulk_processor.call_args[0][1] == [
790+
{"username": TEST_USERNAME, "reactivated": "False"}
791+
]
792+
bulk_processor.assert_called_once()
793+
794+
795+
def test_bulk_reactivate_uses_handler_that_when_encounters_error_increments_total_errors(
796+
runner, mocker, cli_state, worker_stats, get_users_response
797+
):
798+
lines = ["username\n", f"{TEST_USERNAME}\n"]
799+
800+
def _get(username, *args, **kwargs):
801+
if username == "test@example.com":
802+
raise Exception("TEST")
803+
804+
return get_users_response
805+
806+
cli_state.sdk.users.get_by_username.side_effect = _get
807+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
808+
with runner.isolated_filesystem():
809+
with open("test_bulk_reactivate.csv", "w") as csv:
810+
csv.writelines(lines)
811+
runner.invoke(
812+
cli,
813+
["users", "bulk", "reactivate", "test_bulk_reactivate.csv"],
814+
obj=cli_state,
815+
)
816+
handler = bulk_processor.call_args[0][0]
817+
handler(username="test@example.com")
818+
handler(username="not.test@example.com")
819+
assert worker_stats.increment_total_errors.call_count == 1

0 commit comments

Comments
 (0)