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

Feat: command to reset a user's password #15

Merged
merged 5 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ Built on top of [GAMADV-XTD3](https://github.com/taers232c/GAMADV-XTD3) and [GYB

```bash
$ compiler-admin -h
usage: compiler-admin [-h] [-v] {info,init,create,convert,delete,offboard,restore,signout} ...
usage: compiler-admin [-h] [-v] {info,init,create,convert,delete,offboard,reset-password,restore,signout} ...

positional arguments:
{info,init,create,convert,delete,offboard,restore,signout}
{info,init,create,convert,delete,offboard,reset-password,restore,signout}
info Print configuration and debugging information.
init Initialize a new admin project. This command should be run once before any others.
create Create a new user in the Compiler domain.
convert Convert a user account to a new type.
delete Delete a user account.
offboard Offboard a user account.
reset-password Reset a user's password to a randomly generated string.
restore Restore an email backup from a prior offboarding.
signout Signs a user out from all active sessions.

Expand Down
40 changes: 40 additions & 0 deletions compiler_admin/commands/reset_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from argparse import Namespace

from compiler_admin.commands import RESULT_SUCCESS, RESULT_FAILURE
from compiler_admin.commands.signout import signout
from compiler_admin.services.google import USER_HELLO, CallGAMCommand, user_account_name, user_exists


def reset_password(args: Namespace) -> int:
"""Reset a user's password.

Optionally notify an email address with the new randomly generated password.

Args:
username (str): the user account to reset.

notify (str): an email address to send the new password notification.
Returns:
A value indicating if the operation succeeded or failed.
"""
if not hasattr(args, "username"):
raise ValueError("username is required")

account = user_account_name(args.username)

if not user_exists(account):
print(f"User does not exist: {account}")
return RESULT_FAILURE

command = ("update", "user", account, "password", "random", "changepassword")

notify = getattr(args, "notify", None)
if notify:
command += ("notify", notify, "from", USER_HELLO)

print(f"User exists, resetting password: {account}")

res = CallGAMCommand(command)
res += signout(args)

return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
6 changes: 6 additions & 0 deletions compiler_admin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from compiler_admin.commands.info import info
from compiler_admin.commands.init import init
from compiler_admin.commands.offboard import offboard
from compiler_admin.commands.reset_password import reset_password
from compiler_admin.commands.restore import restore
from compiler_admin.commands.signout import signout

Expand Down Expand Up @@ -60,6 +61,9 @@ def _subcmd(name, help, add_username_arg=True) -> argparse.ArgumentParser:
"--force", action="store_true", default=False, help="Don't ask for confirmation before offboarding."
)

reset_parser = _subcmd("reset-password", help="Reset a user's password to a randomly generated string.")
reset_parser.add_argument("--notify", help="An email address to send the newly generated password.")

_subcmd("restore", help="Restore an email backup from a prior offboarding.")

signout_parser = _subcmd("signout", help="Signs a user out from all active sessions.")
Expand All @@ -86,6 +90,8 @@ def _subcmd(name, help, add_username_arg=True) -> argparse.ArgumentParser:
return offboard(args)
elif args.command == "restore":
return restore(args)
elif args.command == "reset-password":
return reset_password(args)
elif args.command == "signout":
return signout(args)

Expand Down
3 changes: 3 additions & 0 deletions compiler_admin/services/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def user_account_name(username: str) -> str:
# Archive account
USER_ARCHIVE = user_account_name("archive")

# Hello account
USER_HELLO = user_account_name("hello")

# Groups
GROUP_PARTNERS = user_account_name("partners")
GROUP_STAFF = user_account_name("staff")
Expand Down
70 changes: 70 additions & 0 deletions tests/commands/test_reset_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from argparse import Namespace
import pytest

from compiler_admin.commands import RESULT_FAILURE, RESULT_SUCCESS
from compiler_admin.commands.reset_password import reset_password, __name__ as MODULE
from compiler_admin.services.google import USER_HELLO


@pytest.fixture
def mock_google_user_exists(mock_google_user_exists):
return mock_google_user_exists(MODULE)


@pytest.fixture
def mock_commands_signout(mock_commands_signout):
return mock_commands_signout(MODULE)


@pytest.fixture
def mock_google_CallGAMCommand(mock_google_CallGAMCommand):
return mock_google_CallGAMCommand(MODULE)


def test_reset_password_user_username_required():
args = Namespace()

with pytest.raises(ValueError, match="username is required"):
reset_password(args)


def test_reset_password_user_does_not_exist(mock_google_user_exists):
mock_google_user_exists.return_value = False

args = Namespace(username="username")
res = reset_password(args)

assert res == RESULT_FAILURE


def test_reset_password_user_exists(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
mock_google_user_exists.return_value = True

args = Namespace(username="username")
res = reset_password(args)

assert res == RESULT_SUCCESS

mock_google_CallGAMCommand.assert_called_once()
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
assert "update user" in call_args
assert "password random changepassword" in call_args

mock_commands_signout.assert_called_once_with(args)


def test_reset_password_notify(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
mock_google_user_exists.return_value = True

args = Namespace(username="username", notify="[email protected]")
res = reset_password(args)

assert res == RESULT_SUCCESS

mock_google_CallGAMCommand.assert_called_once()
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
assert "update user" in call_args
assert "password random changepassword" in call_args
assert f"notify [email protected] from {USER_HELLO}" in call_args

mock_commands_signout.assert_called_once_with(args)
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ def mock_commands_offboard(mock_module_name):
return mock_module_name("offboard")


@pytest.fixture
def mock_commands_reset_password(mock_module_name):
"""Fixture returns a function that patches commands.reset_password in a given module."""
return mock_module_name("reset_password")


@pytest.fixture
def mock_commands_restore(mock_module_name):
"""Fixture returns a function that patches commands.restore in a given module."""
Expand Down
27 changes: 27 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def mock_commands_offboard(mock_commands_offboard):
return mock_commands_offboard(MODULE)


@pytest.fixture
def mock_commands_reset_password(mock_commands_reset_password):
return mock_commands_reset_password(MODULE)


@pytest.fixture
def mock_commands_restore(mock_commands_restore):
return mock_commands_restore(MODULE)
Expand Down Expand Up @@ -177,6 +182,28 @@ def test_main_offboard_no_username(mock_commands_offboard):
assert mock_commands_offboard.call_count == 0


def test_main_reset_password(mock_commands_reset_password):
main(argv=["reset-password", "username"])

mock_commands_reset_password.assert_called_once()
call_args = mock_commands_reset_password.call_args.args
assert Namespace(command="reset-password", username="username", notify=None) in call_args


def test_main_reset_password_notify(mock_commands_reset_password):
main(argv=["reset-password", "username", "--notify", "notification"])

mock_commands_reset_password.assert_called_once()
call_args = mock_commands_reset_password.call_args.args
assert Namespace(command="reset-password", username="username", notify="notification") in call_args


def test_main_reset_password_no_username(mock_commands_reset_password):
with pytest.raises(SystemExit):
main(argv=["reset-password"])
assert mock_commands_reset_password.call_count == 0


def test_main_restore(mock_commands_restore):
main(argv=["restore", "username"])

Expand Down
Loading