diff --git a/epictrack-api/src/api/services/keycloak.py b/epictrack-api/src/api/services/keycloak.py index 642163395..82dc15e37 100644 --- a/epictrack-api/src/api/services/keycloak.py +++ b/epictrack-api/src/api/services/keycloak.py @@ -54,7 +54,6 @@ def get_user_by_email(email: str): users = response.json() if not users: raise ValueError(f"No user found with email: {email}") - print(users) return users @staticmethod @@ -63,16 +62,6 @@ def get_group_members(group_id): response = KeycloakService._request_keycloak(f'groups/{group_id}/members') return response.json() - @staticmethod - def update_user_group(user_id, group_id): - """Update the group of user""" - return KeycloakService._request_keycloak(f'users/{user_id}/groups/{group_id}', HttpMethod.PUT) - - @staticmethod - def delete_user_group(user_id, group_id): - """Delete user-group mapping""" - return KeycloakService._request_keycloak(f'users/{user_id}/groups/{group_id}', HttpMethod.DELETE) - @staticmethod def get_user_groups(user_id): """Get groups of a user by user ID""" diff --git a/epictrack-api/src/api/services/user.py b/epictrack-api/src/api/services/user.py index c74dcccff..dc75e93c8 100644 --- a/epictrack-api/src/api/services/user.py +++ b/epictrack-api/src/api/services/user.py @@ -15,9 +15,7 @@ from flask import current_app import sys -from api.exceptions import BusinessError, PermissionDeniedError from api.services import authorisation -from api.utils import TokenInfo from api.utils.roles import Role as KeycloakRole from .keycloak import KeycloakService @@ -80,70 +78,6 @@ def get_groups(cls): current_app.logger.debug(f"filtered_groups: {filtered_groups}") return filtered_groups - @classmethod - def update_user_group(cls, user_id, user_group_request): - """ - Updates the user's group based on the provided user group request. - - Args: - cls: The class instance. - user_id (str): The ID of the user to update. - user_group_request (dict): A dictionary containing the group update request details. - Expected keys: - - "group_id_to_update" (str): The ID of the group to update. - Raises: - PermissionDeniedError: If the requester does not have permission to update the group. - Returns: - dict: The result of the group update operation from KeycloakService. - """ - cls._check_auth() - token_groups = TokenInfo.get_user_data()["groups"] - groups = cls.get_groups() - requesters_group = next( - ( - group - for group in groups - if group["name"] in token_groups - ), - None, - ) - updating_group = next( - ( - group - for group in groups - if group["id"] == user_group_request.get("group_id_to_update") - ), - None, - ) - if ( - not requesters_group - and not updating_group - and int(UserService._get_level(requesters_group)) - < int(UserService._get_level(updating_group)) - ): - raise PermissionDeniedError("Permission denied") - - UserService._delete_from_all_epictrack_subgroups(user_id) - - result = KeycloakService.update_user_group( - user_id, user_group_request["group_id_to_update"] - ) - return result - - @staticmethod - def _delete_from_all_epictrack_subgroups(user_id): - """Delete all subgroups of 'epictrack' for a user""" - groups = KeycloakService.get_user_groups(user_id) - - # Find the main group 'epictrack' and get its subgroups - track_subgroups = [group for group in groups if 'track/' in group['path'].lower()] - - for subgroup in track_subgroups: - result = KeycloakService.delete_user_group(user_id, subgroup['id']) - - if result.status_code != 204: - raise BusinessError("Error removing group", 500) - @classmethod def _get_level(cls, group): """ diff --git a/epictrack-api/tests/unit/services/test_user.py b/epictrack-api/tests/unit/services/test_user.py index 0e508488f..310c7a7ca 100644 --- a/epictrack-api/tests/unit/services/test_user.py +++ b/epictrack-api/tests/unit/services/test_user.py @@ -1,11 +1,10 @@ """Unit tests for User Service.""" import sys -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from api.services.user import UserService -from api.exceptions import BusinessError class TestGetAllUsers: @@ -132,84 +131,6 @@ def test_handles_empty_subgroups(self, mock_app, mock_keycloak, mock_check_auth) assert len(result) == 0 -class TestUpdateUserGroup: - """Tests for update_user_group method.""" - - @patch("api.services.user.UserService._delete_from_all_epictrack_subgroups") - @patch("api.services.user.UserService.get_groups") - @patch("api.services.user.TokenInfo") - @patch("api.services.user.KeycloakService") - @patch("api.services.user.UserService._check_auth") - def test_updates_user_group( - self, mock_check_auth, mock_keycloak, mock_token, mock_get_groups, mock_delete - ): - """Test updating user's group.""" - user_id = "user123" - user_group_request = {"group_id_to_update": "new-group-id"} - - mock_token.get_user_data.return_value = {"groups": ["Admin"]} - mock_groups = [ - {"id": "new-group-id", "name": "NewGroup", "attributes": {"level": ["5"]}}, - {"id": "old-group-id", "name": "Admin", "attributes": {"level": ["10"]}}, - ] - mock_get_groups.return_value = mock_groups - mock_keycloak.update_user_group.return_value = {"success": True} - - UserService.update_user_group(user_id, user_group_request) - - mock_delete.assert_called_once_with(user_id) - mock_keycloak.update_user_group.assert_called_once_with(user_id, "new-group-id") - - -class TestDeleteFromAllEpictrackSubgroups: - """Tests for _delete_from_all_epictrack_subgroups method.""" - - @patch("api.services.user.KeycloakService") - def test_deletes_all_track_subgroups(self, mock_keycloak): - """Test deleting user from all TRACK subgroups.""" - user_id = "user123" - mock_groups = [ - {"id": "group1", "path": "/TRACK/Admin"}, - {"id": "group2", "path": "/TRACK/User"}, - {"id": "group3", "path": "/OTHER/Group"}, - ] - mock_keycloak.get_user_groups.return_value = mock_groups - - mock_response = MagicMock() - mock_response.status_code = 204 - mock_keycloak.delete_user_group.return_value = mock_response - - UserService._delete_from_all_epictrack_subgroups(user_id) - - # Should only delete from TRACK groups (2 calls) - assert mock_keycloak.delete_user_group.call_count == 2 - - @patch("api.services.user.KeycloakService") - def test_raises_error_on_delete_failure(self, mock_keycloak): - """Test raises error when delete fails.""" - user_id = "user123" - mock_groups = [{"id": "group1", "path": "/TRACK/Admin"}] - mock_keycloak.get_user_groups.return_value = mock_groups - - mock_response = MagicMock() - mock_response.status_code = 500 - mock_keycloak.delete_user_group.return_value = mock_response - - with pytest.raises(BusinessError): - UserService._delete_from_all_epictrack_subgroups(user_id) - - @patch("api.services.user.KeycloakService") - def test_handles_no_track_groups(self, mock_keycloak): - """Test handling user with no TRACK groups.""" - user_id = "user123" - mock_groups = [{"id": "group1", "path": "/OTHER/Group"}] - mock_keycloak.get_user_groups.return_value = mock_groups - - UserService._delete_from_all_epictrack_subgroups(user_id) - - mock_keycloak.delete_user_group.assert_not_called() - - class TestGetLevel: """Tests for _get_level method.""" diff --git a/epictrack-web/src/components/AppHelpButton/HelpPageMap.json b/epictrack-web/src/components/AppHelpButton/HelpPageMap.json index 5193a95d3..3c04033c3 100644 --- a/epictrack-web/src/components/AppHelpButton/HelpPageMap.json +++ b/epictrack-web/src/components/AppHelpButton/HelpPageMap.json @@ -157,10 +157,6 @@ { "epicTrackPath": "/list-management/projects", "helpPage": "https://intranet.gov.bc.ca/intranet/content?id=165BCE74CDFB458F894E6826D7664B12" - }, - { - "epicTrackPath": "/admin/users", - "helpPage": "https://intranet.gov.bc.ca/intranet/content?id=D198F12C1FF743E3A7BE8A0A8A5E2AA9" } ] } diff --git a/epictrack-web/src/components/layout/SideNav/SideNavElements.tsx b/epictrack-web/src/components/layout/SideNav/SideNavElements.tsx index da15c4ad8..3bc42c6b9 100644 --- a/epictrack-web/src/components/layout/SideNav/SideNavElements.tsx +++ b/epictrack-web/src/components/layout/SideNav/SideNavElements.tsx @@ -122,12 +122,6 @@ export const Routes: RouteType[] = [ icon: "GearIcon", group: "Group5", routes: [ - { - name: "Users", - path: "/admin/users", - allowedRoles: [ROLES.MANAGE_USERS], - isAuthenticated: true, - }, { name: "Settings", path: "/admin/settings", diff --git a/epictrack-web/src/components/user/UserList.tsx b/epictrack-web/src/components/user/UserList.tsx deleted file mode 100644 index 3b6575e61..000000000 --- a/epictrack-web/src/components/user/UserList.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { FC, useCallback, useEffect, useMemo, useState } from "react"; -import { type MRT_ColumnDef } from "material-react-table"; -import { Box, IconButton, FormHelperText, Grid, Tooltip } from "@mui/material"; -import { Edit } from "@mui/icons-material"; -import { Group, User } from "../../models/user"; -import { RESULT_STATUS } from "../../constants/application-constant"; -import UserService from "../../services/userService"; -import { ETPageContainer, IButton } from "../shared"; -import Select from "react-select"; -import MasterTrackTable, { - MaterialReactTableProps, -} from "../shared/MasterTrackTable"; -import { UserGroupUpdate } from "../../services/userService/type"; -import { useAppSelector } from "../../hooks"; -import { ColumnFilter } from "components/shared/MasterTrackTable/type"; -import { searchFilter } from "components/shared/MasterTrackTable/filters"; -import { exportToCsv } from "components/shared/MasterTrackTable/utils"; -import Icons from "components/icons"; -import { IconProps } from "components/icons/type"; - -const DownloadIcon: FC = Icons["DownloadIcon"]; - -const UserList = () => { - const [columnFilters, setColumnFilters] = useState([]); - const [groups, setGroups] = useState([]); - const [isValidGroup, setIsValidGroup] = useState(true); - const [resultStatus, setResultStatus] = useState(); - const [selectedGroup, setSelectedGroup] = useState< - Group | undefined | null - >(); - const [users, setUsers] = useState([]); - const userDetails = useAppSelector((state) => state.user.userDetail); - - const getUsers = useCallback(async () => { - setResultStatus(RESULT_STATUS.LOADING); - try { - const userResult = await UserService.getUsers(); - if (userResult.status === 200) { - setUsers(userResult.data as never); - } - } catch (error) { - console.error("User List: ", error); - } finally { - setResultStatus(RESULT_STATUS.LOADED); - } - }, []); - - const getGroups = useCallback(async () => { - try { - const groupResult = await UserService.getGroups(); - if (groupResult.status === 200) { - setGroups(groupResult.data as Group[]); - } - } catch (error) { - console.error("Group List: ", error); - } - }, []); - - useEffect(() => { - getUsers(); - }, [getUsers]); - - useEffect(() => { - getGroups(); - }, [getGroups]); - - const currentUserGroup = useMemo(() => { - return groups - .filter((p) => userDetails.groups.includes(p.path)) - .sort((a, b) => b.level - a.level)[0]; - }, [userDetails, groups]); - - const columns = useMemo[]>( - () => [ - { - id: "name", - accessorFn: (row: User) => `${row.last_name}, ${row.first_name}`, - header: "Name", - enableEditing: false, - filterFn: searchFilter, - }, - { - id: "group.display_name", - accessorFn: (row: User) => row.group?.display_name || "", - header: "Group", - enableEditing: true, - Edit: ({ cell }) => ( - <> -