diff --git a/api_app/api/routes/workspace_users.py b/api_app/api/routes/workspace_users.py index 9ac04552a..7548eb61c 100644 --- a/api_app/api/routes/workspace_users.py +++ b/api_app/api/routes/workspace_users.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends, Response, status from api.dependencies.workspaces import get_workspace_by_id_from_path -from models.domain.authentication import AssignmentType +from models.schemas.workspace_users import UserRoleAssignmentRequest +from models.domain.workspace_users import AssignmentType from resources import strings from services.authentication import get_access_service from models.schemas.users import UsersInResponse, AssignableUsersInResponse @@ -31,13 +32,14 @@ async def get_workspace_roles(workspace=Depends(get_workspace_by_id_from_path), @workspaces_users_admin_router.post("/workspaces/{workspace_id}/users/assign", status_code=status.HTTP_202_ACCEPTED, name=strings.API_ASSIGN_WORKSPACE_USER) -async def assign_workspace_user(response: Response, user_id: str, role_id: str, workspace=Depends(get_workspace_by_id_from_path), access_service=Depends(get_access_service)) -> UsersInResponse: - - access_service.assign_workspace_user( - user_id, - workspace, - role_id - ) +async def assign_workspace_user(response: Response, userRoleAssignmentRequest: UserRoleAssignmentRequest, workspace=Depends(get_workspace_by_id_from_path), access_service=Depends(get_access_service)) -> UsersInResponse: + + for user_id in userRoleAssignmentRequest.user_ids: + access_service.assign_workspace_user( + user_id, + workspace, + userRoleAssignmentRequest.role_id + ) users = access_service.get_workspace_users(workspace) return UsersInResponse(users=users) diff --git a/api_app/models/domain/authentication.py b/api_app/models/domain/authentication.py index 6f14cc8e2..3720350fa 100644 --- a/api_app/models/domain/authentication.py +++ b/api_app/models/domain/authentication.py @@ -1,7 +1,6 @@ from collections import namedtuple from typing import List from pydantic import BaseModel, Field -from enum import Enum RoleAssignment = namedtuple("RoleAssignment", "resource_id, role_id") @@ -11,43 +10,6 @@ class User(BaseModel): email: str = Field(None) roles: List[str] = Field([]) roleAssignments: List[RoleAssignment] = Field([]) - -class Role(BaseModel): - id: str - value: str - isEnabled: bool - email: str = Field(None) - allowedMemberTypes: List[str] = Field([]) - description: str - displayName: str - origin: str - roleAssignments: List[RoleAssignment] = Field([]) - -class AssignableUser(BaseModel): - id: str - displayName: str - userPrincipalName: str - -class AssignmentType(Enum): - APP_ROLE = "ApplicationRole" - GROUP = "Group" - -class AssignedRole(BaseModel): - id: str - displayName: str - type: AssignmentType - - def __eq__(self, other): - return self.id == other.id - - def __hash__(self): - return hash(self.id) - -class AssignedUser(BaseModel): - id: str - displayName: str - userPrincipalName: str - roles: List[AssignedRole] = Field([]) diff --git a/api_app/models/domain/workspace_users.py b/api_app/models/domain/workspace_users.py new file mode 100644 index 000000000..75cbe7231 --- /dev/null +++ b/api_app/models/domain/workspace_users.py @@ -0,0 +1,29 @@ +from typing import List +from pydantic import BaseModel, Field +from enum import Enum + +class AssignableUser(BaseModel): + id: str + displayName: str + userPrincipalName: str + +class AssignmentType(Enum): + APP_ROLE = "ApplicationRole" + GROUP = "Group" + +class Role(BaseModel): + id: str + displayName: str + type: AssignmentType + + def __eq__(self, other): + return self.id == other.id + + def __hash__(self): + return hash(self.id) + +class AssignedUser(BaseModel): + id: str + displayName: str + userPrincipalName: str + roles: List[Role] = Field([]) \ No newline at end of file diff --git a/api_app/models/schemas/roles.py b/api_app/models/schemas/roles.py index 41bb437a3..03e4354b0 100644 --- a/api_app/models/schemas/roles.py +++ b/api_app/models/schemas/roles.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, Field from typing import List -from models.domain.authentication import Role +from models.domain.workspace_users import Role class RolesInResponse(BaseModel): diff --git a/api_app/models/schemas/users.py b/api_app/models/schemas/users.py index fc0f32888..7ca0383e0 100644 --- a/api_app/models/schemas/users.py +++ b/api_app/models/schemas/users.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, Field from typing import List -from models.domain.authentication import AssignedUser, AssignableUser +from models.domain.workspace_users import AssignedUser, AssignableUser class UsersInResponse(BaseModel): diff --git a/api_app/models/schemas/workspace_users.py b/api_app/models/schemas/workspace_users.py new file mode 100644 index 000000000..da1d746d1 --- /dev/null +++ b/api_app/models/schemas/workspace_users.py @@ -0,0 +1,15 @@ +from typing import List +from pydantic import BaseModel, Field + + +class UserRoleAssignmentRequest(BaseModel): + role_id: str = Field(title="Role Id", description="Role to assign users to") + user_ids: List[str] = Field([], title="List of User Ids", description="List of User Ids to assign the role to") + + class Config: + schema_extra = { + "example": { + "role_id": "1234", + "user_ids": ["1", "2"] + } + } \ No newline at end of file diff --git a/api_app/services/aad_authentication.py b/api_app/services/aad_authentication.py index 6f8813237..1a7554f28 100644 --- a/api_app/services/aad_authentication.py +++ b/api_app/services/aad_authentication.py @@ -12,7 +12,8 @@ from services.access_service import AccessService, AuthConfigValidationError, UserRoleAssignmentError from core import config from db.errors import EntityDoesNotExist -from models.domain.authentication import AssignedRole, AssignedUser, AssignmentType, User, AssignableUser, Role, RoleAssignment +from models.domain.authentication import User, RoleAssignment +from models.domain.workspace_users import AssignedUser, AssignmentType, AssignableUser, Role from models.domain.workspace import Workspace, WorkspaceRole from resources import strings from db.repositories.workspaces import WorkspaceRepository @@ -264,11 +265,11 @@ def _get_user_details(self, roles_graph_data, msgraph_token): return users_graph_data - def _get_roles_for_principal(self, user_id, roles_graph_data, app_id_to_role_name, assignmentType: AssignmentType = AssignmentType.APP_ROLE) -> List[AssignedRole]: + def _get_roles_for_principal(self, user_id, roles_graph_data, app_id_to_role_name, assignmentType: AssignmentType = AssignmentType.APP_ROLE) -> List[Role]: roles = [] for role_assignment in roles_graph_data["value"]: if role_assignment["principalId"] == user_id: - roles.append(AssignedRole(id=role_assignment["appRoleId"], displayName=app_id_to_role_name[role_assignment["appRoleId"]], type=assignmentType)) + roles.append(Role(id=role_assignment["appRoleId"], displayName=app_id_to_role_name[role_assignment["appRoleId"]], type=assignmentType)) return roles def _get_users_inc_groups_from_response(self, users_graph_data, roles_graph_data, app_id_to_role_name) -> List[AssignedUser]: @@ -340,13 +341,14 @@ def get_workspace_roles(self, workspace: Workspace) -> List[Role]: roles = [] + roleAssignmentType = AssignmentType.APP_ROLE + if self._is_workspace_role_group_in_use(workspace): + roleAssignmentType = AssignmentType.GROUP + for role in graph_data["value"]: - roles.append(Role(id=role["id"], value=role["value"], - isEnabled=role["isEnabled"], - description=role["description"], + roles.append(Role(id=role["id"], displayName=role["displayName"], - origin=role["origin"], - allowedMemberTypes=role["allowedMemberTypes"])) + type=roleAssignmentType)) return roles @@ -361,21 +363,6 @@ def get_workspace_user_emails_by_role_assignment(self, workspace: Workspace): workspace_role_assignments_details[role].append(user.email) return workspace_role_assignments_details - def get_workspace_role_by_name(self, name: str, workspace: Workspace) -> Role: - app_roles_endpoint = f"{MICROSOFT_GRAPH_URL}/v1.0/servicePrincipals/{workspace.properties['sp_id']}/appRoles" - graph_data = self._ms_graph_query(app_roles_endpoint, "GET") - - for role in graph_data["value"]: - if role["value"] == name: - return Role(id=role["id"], value=role["value"], - isEnabled=role["isEnabled"], - description=role["description"], - displayName=role["displayName"], - origin=role["origin"], - allowedMemberTypes=role["allowedMemberTypes"]) - - return None - def assign_workspace_user(self, user_id: str, workspace: Workspace, role_id: str) -> None: # User already has the role, do nothing if self._is_user_in_role(user_id, role_id): @@ -385,7 +372,7 @@ def assign_workspace_user(self, user_id: str, workspace: Workspace, role_id: str else: return self._assign_workspace_user_to_application(user_id, workspace, role_id) - def _is_user_in_role(self, user_id: User, role_id: Role) -> bool: + def _is_user_in_role(self, user_id: str, role_id: str) -> bool: user_app_role_query = f"{MICROSOFT_GRAPH_URL}/v1.0/users/{user_id}/appRoleAssignments" user_app_roles = self._ms_graph_query(user_app_role_query, "GET") return any(r for r in user_app_roles["value"] if r["appRoleId"] == role_id) diff --git a/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx b/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx index 9f0f2c48c..87ec88705 100644 --- a/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx +++ b/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx @@ -75,15 +75,15 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent { if (items && items.length > 0) { - setSelectedUser(items[0].key as string); + setSelectedUsers(items.map(item => item.key as string)); } else { - setSelectedUser(null); + setSelectedUsers(null); } }; const [roleOptions, setRoleOptions] = useState([]); - const [selectedUser, setSelectedUser] = useState(null); + const [selectedUsers, setSelectedUsers] = useState(null); const [selectedRole, setSelectedRole] = useState(null); const [assigning, setAssigning] = useState(false); const [hasAssignmentError, setHasAssignmentError] = useState(false); @@ -120,11 +120,12 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent { setAssigning(true); - const encodedUser = selectedUser?.replaceAll('#', '%23'); - const scopeId = ""; try { - const response = await apiCall(`${ApiEndpoint.Workspaces}/${workspace.id}/${ApiEndpoint.Users}/assign?user_id=${selectedUser}&role_id=${selectedRole}`, HttpMethod.Post, scopeId); + const response = await apiCall(`${ApiEndpoint.Workspaces}/${workspace.id}/${ApiEndpoint.Users}/assign`, HttpMethod.Post, scopeId, { + role_id: selectedRole, + user_ids: selectedUsers + }); props.onAssignUser(response); } catch (err: any) { @@ -134,21 +135,21 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent { let footer = <> footer = <>
- assign()} disabled={assigning || (!selectedUser || !selectedRole)}>Assign + assign()} disabled={assigning || (!selectedUsers || !selectedRole)}>Assign
return footer; - }, [selectedUser, selectedRole, assign, assigning]); + }, [selectedUsers, selectedRole, assign, assigning]); return (