diff --git a/CHANGELOG.md b/CHANGELOG.md index 605d8195..8611ffe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Remove (internal) obsolete `do_not_separate_logs` argument (\#738). * Add `group {add|remove}-user` commands, and deprecate `--new-user-ids` argument from `group update` (\#748). * Update `user whoami --viewer-paths` to call the new dedicated [server endpoint](https://github.com/fractal-analytics-platform/fractal-server/pull/2096) (\#748). +* Add `user set-groups` commands (\#753). * Testing: * Align with fractal-server 2.9.0 removal of `DB_ENGINE` variable (\#743). diff --git a/fractal_client/cmd/__init__.py b/fractal_client/cmd/__init__.py index 35d2be2a..3a8690a2 100644 --- a/fractal_client/cmd/__init__.py +++ b/fractal_client/cmd/__init__.py @@ -33,6 +33,7 @@ from ._user import user_edit from ._user import user_list from ._user import user_register +from ._user import user_set_groups from ._user import user_show from ._user import user_whoami from ._workflow import delete_workflow @@ -361,6 +362,13 @@ def user( ] function_kwargs = get_kwargs(parameters, kwargs) iface = user_edit(client, **function_kwargs) + elif subcmd == "set-groups": + parameters = [ + "user_id", + "group_ids", + ] + function_kwargs = get_kwargs(parameters, kwargs) + iface = user_set_groups(client, **function_kwargs) elif subcmd == "whoami": parameters = ["viewer_paths"] function_kwargs = get_kwargs(parameters, kwargs) diff --git a/fractal_client/cmd/_user.py b/fractal_client/cmd/_user.py index 2a6f57bc..e925ee1a 100644 --- a/fractal_client/cmd/_user.py +++ b/fractal_client/cmd/_user.py @@ -205,6 +205,17 @@ def user_edit( return Interface(retcode=0, data=new_user_with_settings) +def user_set_groups( + client: AuthClient, *, user_id: int, group_ids: list[int] +) -> Interface: + res = client.post( + f"{settings.FRACTAL_SERVER}/auth/users/{user_id}/set-groups/", + json=dict(group_ids=group_ids), + ) + user = check_response(res, expected_status_code=200) + return Interface(retcode=0, data=user) + + def user_whoami( client: AuthClient, *, batch: bool, viewer_paths: bool = False ) -> Interface: diff --git a/fractal_client/parser.py b/fractal_client/parser.py index 55501fe3..ee0e8614 100644 --- a/fractal_client/parser.py +++ b/fractal_client/parser.py @@ -912,6 +912,27 @@ required=False, ) +# user set-groups +user_set_groups_parser = user_subparsers.add_parser( + "set-groups", + description=( + "Reset user-group membership for an existing user." + ), + allow_abbrev=False, +) +user_set_groups_parser.add_argument( + "user_id", help="ID of the user.", type=int +) +user_set_groups_parser.add_argument( + "group_ids", + help=( + "List of the IDs of groups we want the user to be member. " + "WARNING: this list replaces the current group memberships." + ), + type=int, + nargs="+", +) + # (USER)GROUP GROUP diff --git a/poetry.lock b/poetry.lock index 7520f898..c86d5112 100644 --- a/poetry.lock +++ b/poetry.lock @@ -696,7 +696,7 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "fractal-server" -version = "2.9.0a10" +version = "2.9.0a11" description = "Server component of the Fractal analytics platform" optional = false python-versions = "^3.10" @@ -726,7 +726,7 @@ uvicorn-worker = "^0.2.0" type = "git" url = "https://github.com/fractal-analytics-platform/fractal-server.git" reference = "main" -resolved_reference = "8bb57912219c0b87997249d730c7f0413dcf8478" +resolved_reference = "0ea8af11f425d4d32aa5e7bde90bf9fdd6105762" [[package]] name = "ghp-import" diff --git a/tests/test_group.py b/tests/test_group.py index 4ba36d7a..17a33f25 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -113,14 +113,18 @@ def test_group_commands( res = invoke_as_superuser("group list --user-ids") assert len(res.data) == initial_number_of_groups + 2 - assert res.data[0]["id"] == default_group_id - assert len(res.data[0]["user_ids"]) == initial_number_of_users + 3 - assert res.data[-2]["id"] == group1_id - assert set(res.data[1]["user_ids"]) == set([user1_id, user2_id]) - assert res.data[-1]["id"] == group2_id - assert set(res.data[2]["user_ids"]) == set( - [user3_id, user2_id, superuser_id] + users_default_group = next( + g["user_ids"] for g in res.data if g["id"] == default_group_id + ) + assert len(users_default_group) == initial_number_of_users + 3 + users_group_1 = next( + g["user_ids"] for g in res.data if g["id"] == group1_id + ) + assert set(users_group_1) == set([user1_id, user2_id]) + users_group_2 = next( + g["user_ids"] for g in res.data if g["id"] == group2_id ) + assert set(users_group_2) == set([user3_id, user2_id, superuser_id]) # Remove users from group res = invoke_as_superuser(f"group remove-user {group2_id} {user3_id}") diff --git a/tests/test_user.py b/tests/test_user.py index cce851da..ea5e4c59 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -2,6 +2,7 @@ import pytest from devtools import debug +from fractal_server.app.security import FRACTAL_DEFAULT_GROUP_NAME PWD_USER = "1234" @@ -383,3 +384,76 @@ def test_whoami_as_superuser(invoke_as_superuser, superuser): debug(res.data) assert res.data["email"] == superuser["email"] assert res.data["is_superuser"] + + +def test_user_set_groups(invoke_as_superuser, user_factory, new_name): + # get default group + res = invoke_as_superuser("group list --user-ids") + default_group = next( + group + for group in res.data + if group["name"] == FRACTAL_DEFAULT_GROUP_NAME + ) + default_group_id = default_group["id"] + # create 2 new users + user1 = user_factory(email=f"{new_name()}@example.org", password="psw1") + assert len(user1["group_ids_names"]) == 1 + user1_id = user1["id"] + user2 = user_factory(email=f"{new_name()}@example.org", password="psw2") + assert len(user2["group_ids_names"]) == 1 + user2_id = user2["id"] + # create 2 new groups + group1 = invoke_as_superuser(f"group new {new_name()}") + assert len(group1.data["user_ids"]) == 0 + group1_id = group1.data["id"] + group2 = invoke_as_superuser(f"group new {new_name()}") + assert len(group2.data["user_ids"]) == 0 + group2_id = group2.data["id"] + + with pytest.raises(SystemExit): + # no arguments + invoke_as_superuser("user set-groups") + with pytest.raises(SystemExit): + # no group_ids list + invoke_as_superuser(f"user set-groups {user1_id}") + with pytest.raises(SystemExit): + # group_ids must be a list of integers + invoke_as_superuser(f"user set-groups {user1_id} {group1_id} foo") + with pytest.raises(SystemExit): + # there must always be the default group id in group_ids + invoke_as_superuser(f"user set-groups {user1_id} {group1_id}") + with pytest.raises(SystemExit): + # repeated elements in group_ids are forbidden + invoke_as_superuser( + "user set-groups " + f"{user1_id} {default_group_id} {group1_id} {group1_id}" + ) + + # Add user1 to group1 + res = invoke_as_superuser( + f"user set-groups {user1_id} {group1_id} {default_group_id}" + ) + assert len(res.data["group_ids_names"]) == 2 + group1 = invoke_as_superuser(f"group get {group1_id}") + assert len(group1.data["user_ids"]) == 1 + + # Add user2 to group1 and group2 + res = invoke_as_superuser( + "user set-groups " + f"{user2_id} {group2_id} {group1_id} {default_group_id}" + ) + assert len(res.data["group_ids_names"]) == 3 + group1 = invoke_as_superuser(f"group get {group1_id}") + assert len(group1.data["user_ids"]) == 2 + group2 = invoke_as_superuser(f"group get {group2_id}") + assert len(group2.data["user_ids"]) == 1 + + # Add user1 to group2 and remove them from group1 + res = invoke_as_superuser( + f"user set-groups {user1_id} {group2_id} {default_group_id}" + ) + assert len(res.data["group_ids_names"]) == 2 + group1 = invoke_as_superuser(f"group get {group1_id}") + assert len(group1.data["user_ids"]) == 1 + group2 = invoke_as_superuser(f"group get {group2_id}") + assert len(group2.data["user_ids"]) == 2