Skip to content

Commit bfbb112

Browse files
committed
feat(user): implement alumni command
middle step before offboarding/deletion
1 parent e215ad7 commit bfbb112

File tree

6 files changed

+229
-1
lines changed

6 files changed

+229
-1
lines changed

compiler_admin/commands/user/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from argparse import Namespace
22

3+
from compiler_admin.commands.user.alumni import alumni # noqa: F401
34
from compiler_admin.commands.user.create import create # noqa: F401
45
from compiler_admin.commands.user.convert import convert # noqa: F401
56
from compiler_admin.commands.user.delete import delete # noqa: F401
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from argparse import Namespace
2+
3+
from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
4+
from compiler_admin.commands.user.reset import reset
5+
from compiler_admin.services.google import (
6+
OU_ALUMNI,
7+
CallGAMCommand,
8+
move_user_ou,
9+
user_account_name,
10+
user_exists,
11+
)
12+
13+
14+
def alumni(args: Namespace) -> int:
15+
"""Convert a user to a Compiler alumni.
16+
17+
Optionally notify an email address with the new randomly generated password.
18+
19+
Args:
20+
username (str): the user account to convert.
21+
22+
notify (str): an email address to send the new password notification.
23+
Returns:
24+
A value indicating if the operation succeeded or failed.
25+
"""
26+
if not hasattr(args, "username"):
27+
raise ValueError("username is required")
28+
29+
account = user_account_name(args.username)
30+
31+
if not user_exists(account):
32+
print(f"User does not exist: {account}")
33+
return RESULT_FAILURE
34+
35+
if getattr(args, "force", False) is False:
36+
cont = input(f"Convert account to alumni for {account}? (Y/n)")
37+
if not cont.lower().startswith("y"):
38+
print("Aborting conversion.")
39+
return RESULT_SUCCESS
40+
41+
res = RESULT_SUCCESS
42+
43+
print("Removing from groups")
44+
res += CallGAMCommand(("user", account, "delete", "groups"))
45+
46+
print(f"Moving to OU: {OU_ALUMNI}")
47+
res += move_user_ou(account, OU_ALUMNI)
48+
49+
# reset password, sign out
50+
res += reset(args)
51+
52+
print("Clearing user profile info")
53+
for prop in ["address", "location", "otheremail", "phone"]:
54+
command = ("update", "user", account, prop, "clear")
55+
res += CallGAMCommand(command)
56+
57+
print("Resetting recovery email")
58+
recovery = getattr(args, "recovery_email", "")
59+
command = ("update", "user", account, "recoveryemail", recovery)
60+
res += CallGAMCommand(command)
61+
62+
print("Resetting recovery phone")
63+
recovery = getattr(args, "recovery_phone", "")
64+
command = ("update", "user", account, "recoveryphone", recovery)
65+
res += CallGAMCommand(command)
66+
67+
print("Turning off 2FA")
68+
command = ("user", account, "turnoff2sv")
69+
res += CallGAMCommand(command)
70+
71+
return res

compiler_admin/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ def setup_user_command(cmd_parsers: _SubParsersAction):
133133
user_cmd.set_defaults(func=user)
134134
user_subcmds = add_sub_cmd_parser(user_cmd, help="The user command to run.")
135135

136+
user_alumni = add_sub_cmd_with_username_arg(user_subcmds, "alumni", help="Convert a user account to a Compiler alumni.")
137+
user_alumni.add_argument("--notify", help="An email address to send the alumni's new password.")
138+
user_alumni.add_argument(
139+
"--force", action="store_true", default=False, help="Don't ask for confirmation before conversion."
140+
)
141+
136142
user_create = add_sub_cmd_with_username_arg(user_subcmds, "create", help="Create a new user in the Compiler domain.")
137143
user_create.add_argument("--notify", help="An email address to send the newly created account info.")
138144

compiler_admin/services/google.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
DOMAIN = "compiler.la"
1818

1919
# Org structure
20+
OU_ALUMNI = "alumni"
2021
OU_CONTRACTORS = "contractors"
2122
OU_STAFF = "staff"
2223
OU_PARTNERS = f"{OU_STAFF}/partners"
@@ -77,7 +78,7 @@ def add_user_to_group(username: str, group: str) -> int:
7778

7879

7980
def move_user_ou(username: str, ou: str) -> int:
80-
"""Remove a user from a group."""
81+
"""Move a user into a new OU."""
8182
return CallGAMCommand(("update", "ou", ou, "move", username))
8283

8384

tests/commands/user/test_alumni.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from argparse import Namespace
2+
import pytest
3+
4+
from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
5+
from compiler_admin.commands.user.alumni import alumni, __name__ as MODULE
6+
from compiler_admin.services.google import OU_ALUMNI
7+
8+
9+
@pytest.fixture
10+
def mock_commands_reset(mock_commands_reset):
11+
return mock_commands_reset(MODULE)
12+
13+
14+
@pytest.fixture
15+
def mock_input_yes(mock_input):
16+
fix = mock_input(MODULE)
17+
fix.return_value = "y"
18+
return fix
19+
20+
21+
@pytest.fixture
22+
def mock_input_no(mock_input):
23+
fix = mock_input(MODULE)
24+
fix.return_value = "n"
25+
return fix
26+
27+
28+
@pytest.fixture
29+
def mock_google_CallGAMCommand(mock_google_CallGAMCommand):
30+
return mock_google_CallGAMCommand(MODULE)
31+
32+
33+
@pytest.fixture
34+
def mock_google_move_user_ou(mock_google_move_user_ou):
35+
return mock_google_move_user_ou(MODULE)
36+
37+
38+
@pytest.fixture
39+
def mock_google_remove_user_from_group(mock_google_remove_user_from_group):
40+
return mock_google_remove_user_from_group(MODULE)
41+
42+
43+
@pytest.fixture
44+
def mock_google_user_exists(mock_google_user_exists):
45+
return mock_google_user_exists(MODULE)
46+
47+
48+
def test_alumni_username_required():
49+
args = Namespace()
50+
51+
with pytest.raises(ValueError, match="username is required"):
52+
alumni(args)
53+
54+
55+
def test_alumni_user_does_not_exists(mock_google_user_exists, mock_google_CallGAMCommand):
56+
mock_google_user_exists.return_value = False
57+
58+
args = Namespace(username="username")
59+
res = alumni(args)
60+
61+
assert res == RESULT_FAILURE
62+
mock_google_CallGAMCommand.assert_not_called()
63+
64+
65+
@pytest.mark.usefixtures("mock_input_yes")
66+
def test_alumni_confirm_yes(
67+
mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou
68+
):
69+
mock_google_user_exists.return_value = True
70+
71+
args = Namespace(username="username", force=False)
72+
res = alumni(args)
73+
74+
assert res == RESULT_SUCCESS
75+
mock_google_CallGAMCommand.assert_called()
76+
mock_google_move_user_ou.assert_called_once_with("[email protected]", OU_ALUMNI)
77+
mock_commands_reset.assert_called_once_with(args)
78+
79+
80+
@pytest.mark.usefixtures("mock_input_no")
81+
def test_alumni_confirm_no(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou):
82+
mock_google_user_exists.return_value = True
83+
84+
args = Namespace(username="username", force=False)
85+
res = alumni(args)
86+
87+
assert res == RESULT_SUCCESS
88+
mock_google_CallGAMCommand.assert_not_called()
89+
mock_commands_reset.assert_not_called()
90+
mock_google_move_user_ou.assert_not_called()
91+
92+
93+
def test_alumni_force(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou):
94+
mock_google_user_exists.return_value = True
95+
96+
args = Namespace(username="username", force=True)
97+
res = alumni(args)
98+
99+
assert res == RESULT_SUCCESS
100+
mock_google_CallGAMCommand.assert_called()
101+
mock_google_move_user_ou.assert_called_once_with("[email protected]", OU_ALUMNI)
102+
mock_commands_reset.assert_called_once_with(args)

tests/test_main.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,53 @@ def test_main_time_convert_output(mock_commands_time):
252252
)
253253

254254

255+
def test_main_user_alumni(mock_commands_user):
256+
main(argv=["user", "alumni", "username"])
257+
258+
mock_commands_user.assert_called_once()
259+
call_args = mock_commands_user.call_args.args
260+
assert (
261+
Namespace(func=mock_commands_user, command="user", subcommand="alumni", username="username", force=False, notify=None)
262+
in call_args
263+
)
264+
265+
266+
def test_main_user_alumni_notify(mock_commands_user):
267+
main(argv=["user", "alumni", "username", "--notify", "notification"])
268+
269+
mock_commands_user.assert_called_once()
270+
call_args = mock_commands_user.call_args.args
271+
assert (
272+
Namespace(
273+
func=mock_commands_user,
274+
command="user",
275+
subcommand="alumni",
276+
username="username",
277+
force=False,
278+
notify="notification",
279+
)
280+
in call_args
281+
)
282+
283+
284+
def test_main_user_alumni_force(mock_commands_user):
285+
main(argv=["user", "alumni", "username", "--force"])
286+
287+
mock_commands_user.assert_called_once()
288+
call_args = mock_commands_user.call_args.args
289+
assert (
290+
Namespace(
291+
func=mock_commands_user,
292+
command="user",
293+
subcommand="alumni",
294+
username="username",
295+
force=True,
296+
notify=None,
297+
)
298+
in call_args
299+
)
300+
301+
255302
def test_main_user_create(mock_commands_user):
256303
main(argv=["user", "create", "username"])
257304

0 commit comments

Comments
 (0)