diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d954297c7..b2912feeda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ENHANCEMENTS: BUG FIXES: * Upgrade unresticted and airlock base template versions due to diagnostic settings retention period being depreciated ([#3704](https://github.com/microsoft/AzureTRE/pull/3704)) +* Enable TRE Admins to view workspace details when don't have a workspace role ([#2363](https://github.com/microsoft/AzureTRE/issues/2363)) * Fix shared services list return restricted resource for admins causing issues with updates ([#3716](https://github.com/microsoft/AzureTRE/issues/3716)) * Fix grey box appearing on resource card when costs are not available. ([#3254](https://github.com/microsoft/AzureTRE/issues/3254)) * Fix notification panel not passing the workspace scope id to the API hence UI not updating ([#3353](https://github.com/microsoft/AzureTRE/issues/3353)) diff --git a/ui/app/package.json b/ui/app/package.json index 3a3a0e90b0..46b545b9c3 100644 --- a/ui/app/package.json +++ b/ui/app/package.json @@ -1,6 +1,6 @@ { "name": "tre-ui", - "version": "0.5.7", + "version": "0.5.8", "private": true, "dependencies": { "@azure/msal-browser": "^2.35.0", diff --git a/ui/app/src/components/shared/ResourceCard.tsx b/ui/app/src/components/shared/ResourceCard.tsx index cf83673afd..b41f5a21f3 100644 --- a/ui/app/src/components/shared/ResourceCard.tsx +++ b/ui/app/src/components/shared/ResourceCard.tsx @@ -12,9 +12,11 @@ import { ResourceType } from '../../models/resourceType'; import { WorkspaceContext } from '../../contexts/WorkspaceContext'; import { CostsTag } from './CostsTag'; import { ConfirmCopyUrlToClipboard } from './ConfirmCopyUrlToClipboard'; +import { AppRolesContext } from '../../contexts/AppRolesContext'; import { SecuredByRole } from './SecuredByRole'; import { RoleName, WorkspaceRoleName } from '../../models/roleNames'; + interface ResourceCardProps { resource: Resource, itemId: number, @@ -83,8 +85,10 @@ export const ResourceCard: React.FunctionComponent = (props: headerBadge = } + const appRoles = useContext(AppRolesContext); const authNotProvisioned = props.resource.resourceType === ResourceType.Workspace && !props.resource.properties.scope_id; - const cardStyles = authNotProvisioned ? noNavCardStyles : clickableCardStyles; + const enableClickOnCard = !authNotProvisioned || appRoles.roles.includes(RoleName.TREAdmin); + const cardStyles = enableClickOnCard ? noNavCardStyles : clickableCardStyles; return ( <> @@ -110,7 +114,7 @@ export const ResourceCard: React.FunctionComponent = (props: {if (!authNotProvisioned) goToResource()}} + onClick={() => {if (enableClickOnCard) goToResource()}} > {props.resource.properties.display_name} diff --git a/ui/app/src/components/workspaces/WorkspaceProvider.tsx b/ui/app/src/components/workspaces/WorkspaceProvider.tsx index c0c4bed4d8..b62b74c0c5 100644 --- a/ui/app/src/components/workspaces/WorkspaceProvider.tsx +++ b/ui/app/src/components/workspaces/WorkspaceProvider.tsx @@ -1,4 +1,4 @@ -import { Spinner, SpinnerSize, Stack } from '@fluentui/react'; +import { FontIcon, Icon, Label, Spinner, SpinnerSize, Stack, getTheme, mergeStyles } from '@fluentui/react'; import React, { useContext, useEffect, useRef, useState } from 'react'; import { Route, Routes, useParams } from 'react-router-dom'; import { ApiEndpoint } from '../../models/apiEndpoints'; @@ -18,17 +18,21 @@ import { Airlock } from '../shared/airlock/Airlock'; import { APIError } from '../../models/exceptions'; import { LoadingState } from '../../models/loadingState'; import { ExceptionLayout } from '../shared/ExceptionLayout'; +import { AppRolesContext } from '../../contexts/AppRolesContext'; +import { RoleName } from '../../models/roleNames'; export const WorkspaceProvider: React.FunctionComponent = () => { const apiCall = useAuthApiCall(); const [selectedWorkspaceService, setSelectedWorkspaceService] = useState({} as WorkspaceService); - const [workspaceServices, setWorkspaceServices] = useState([] as Array) - const [sharedServices, setSharedServices] = useState([] as Array) + const [workspaceServices, setWorkspaceServices] = useState([] as Array); + const [sharedServices, setSharedServices] = useState([] as Array); const workspaceCtx = useRef(useContext(WorkspaceContext)); const [loadingState, setLoadingState] = useState(LoadingState.Loading); - const [ apiError, setApiError ] = useState({} as APIError); + const [apiError, setApiError] = useState({} as APIError); const { workspaceId } = useParams(); + const appRoles = useContext(AppRolesContext); + const refIsTREAdminUser = useRef(false); // set workspace context from url useEffect(() => { @@ -36,36 +40,58 @@ export const WorkspaceProvider: React.FunctionComponent = () => { try { // get the workspace - first we get the scope_id so we can auth against the right aad app let scopeId = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}/scopeid`, HttpMethod.Get)).workspaceAuth.scopeId; - if (scopeId === "") { - console.error("Unable to get scope_id from workspace - authentication not set up."); - } - const ws = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get, scopeId)).workspace; - workspaceCtx.current.setWorkspace(ws); - const ws_application_id_uri = ws.properties.scope_id; + const authProvisioned = scopeId !== ""; - // use the client ID to get a token against the workspace (tokenOnly), and set the workspace roles in the context let wsRoles: Array = []; - console.log('Getting workspace'); - await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get, ws_application_id_uri, undefined, ResultType.JSON, (roles: Array) => { - workspaceCtx.current.setRoles(roles); - wsRoles = roles; - }, true); - - // get workspace services to pass to nav + ws services page - const workspaceServices = await apiCall(`${ApiEndpoint.Workspaces}/${ws.id}/${ApiEndpoint.WorkspaceServices}`, HttpMethod.Get, ws_application_id_uri); - setWorkspaceServices(workspaceServices.workspaceServices); - setLoadingState(wsRoles && wsRoles.length > 0 ? LoadingState.Ok : LoadingState.AccessDenied); - - // get shared services to pass to nav shared services pages - const sharedServices = await apiCall(ApiEndpoint.SharedServices, HttpMethod.Get); - setSharedServices(sharedServices.sharedServices); - - } catch (e: any){ - e.userMessage = 'Error retrieving workspace'; - setApiError(e); - setLoadingState(LoadingState.Error); + let ws: Workspace = {} as Workspace; + + if (authProvisioned) { + // use the client ID to get a token against the workspace (tokenOnly), and set the workspace roles in the context + await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get, scopeId, + undefined, ResultType.JSON, (roles: Array) => { + wsRoles = roles; + }, true); + } + + if (wsRoles && wsRoles.length > 0) { + ws = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get, scopeId)).workspace; + workspaceCtx.current.setWorkspace(ws); + workspaceCtx.current.setRoles(wsRoles); + + // get workspace services to pass to nav + ws services page + const workspaceServices = await apiCall(`${ApiEndpoint.Workspaces}/${ws.id}/${ApiEndpoint.WorkspaceServices}`, + HttpMethod.Get, ws.properties.scope_id); + setWorkspaceServices(workspaceServices.workspaceServices); + setLoadingState(LoadingState.Ok); + // get shared services to pass to nav shared services pages + const sharedServices = await apiCall(ApiEndpoint.SharedServices, HttpMethod.Get); + setSharedServices(sharedServices.sharedServices); + } else if (appRoles.roles.includes(RoleName.TREAdmin)) { + + ws = (await apiCall(`${ApiEndpoint.Workspaces}/${workspaceId}`, HttpMethod.Get)).workspace; + workspaceCtx.current.setWorkspace(ws); + setLoadingState(LoadingState.Ok); + refIsTREAdminUser.current = true; + } else { + let e = new APIError(); + e.status = 403; + e.userMessage = "User does not have a role assigned in the workspace or the TRE Admin role assigned"; + e.endpoint = `${ApiEndpoint.Workspaces}/${workspaceId}`; + throw e; + } + + } catch (e: any) { + if (e.status === 401 || e.status === 403) { + setApiError(e); + setLoadingState(LoadingState.AccessDenied); + } else { + e.userMessage = 'Error retrieving workspace'; + setApiError(e); + setLoadingState(LoadingState.Error); + } } + }; getWorkspace(); @@ -76,28 +102,27 @@ export const WorkspaceProvider: React.FunctionComponent = () => { ctx.setRoles([]); ctx.setWorkspace({} as Workspace); }); - }, [apiCall, workspaceId]); + }, [apiCall, workspaceId, appRoles.roles, loadingState]); const addWorkspaceService = (w: WorkspaceService) => { - let ws = [...workspaceServices] + let ws = [...workspaceServices]; ws.push(w); setWorkspaceServices(ws); - } + }; const updateWorkspaceService = (w: WorkspaceService) => { let i = workspaceServices.findIndex((f: WorkspaceService) => f.id === w.id); - let ws = [...workspaceServices] + let ws = [...workspaceServices]; ws.splice(i, 1, w); setWorkspaceServices(ws); - } + }; const removeWorkspaceService = (w: WorkspaceService) => { let i = workspaceServices.findIndex((f: WorkspaceService) => f.id === w.id); let ws = [...workspaceServices]; - console.log("removing WS...", ws[i]); ws.splice(i, 1); setWorkspaceServices(ws); - } + }; switch (loadingState) { case LoadingState.Ok: @@ -105,47 +130,67 @@ export const WorkspaceProvider: React.FunctionComponent = () => { <> - - setSelectedWorkspaceService(ws)} - addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)} /> - + {!refIsTREAdminUser.current && ( + + setSelectedWorkspaceService(ws)} + addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)} /> + + )} + - setSelectedWorkspaceService(ws)} - addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)} - updateWorkspaceService={(ws: WorkspaceService) => updateWorkspaceService(ws)} - removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} /> - } /> - setSelectedWorkspaceService(ws)} - addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)} - updateWorkspaceService={(ws: WorkspaceService) => updateWorkspaceService(ws)} - removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} - /> - } /> - updateWorkspaceService(ws)} - removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} /> - } /> - - } /> - - } /> - - } /> + {!refIsTREAdminUser.current ? ( + setSelectedWorkspaceService(ws)} + addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)} + updateWorkspaceService={(ws: WorkspaceService) => updateWorkspaceService(ws)} + removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} /> + ) : ( + + + + You are currently accessing this workspace using a TRE Admin role. Additional funcitonality requires a workspace role, such as Workspace Owner. + + + )} + } + /> + {!refIsTREAdminUser.current && ( + <> + setSelectedWorkspaceService(ws)} + addWorkspaceService={(ws: WorkspaceService) => addWorkspaceService(ws)} + updateWorkspaceService={(ws: WorkspaceService) => updateWorkspaceService(ws)} + removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} + /> + } /> + updateWorkspaceService(ws)} + removeWorkspaceService={(ws: WorkspaceService) => removeWorkspaceService(ws)} /> + } /> + + + } /> + + } /> + + } /> + + )} @@ -154,14 +199,22 @@ export const WorkspaceProvider: React.FunctionComponent = () => { ); case LoadingState.Error: + case LoadingState.AccessDenied: return ( - ) + ); default: return (
- ) + ); } }; + +const { palette } = getTheme(); +const warningIcon = mergeStyles({ + color: palette.orangeLight, + fontSize: 18, + marginRight: 8 +}); diff --git a/ui/app/src/hooks/useAuthApiCall.ts b/ui/app/src/hooks/useAuthApiCall.ts index 83782afd4f..0de6472141 100644 --- a/ui/app/src/hooks/useAuthApiCall.ts +++ b/ui/app/src/hooks/useAuthApiCall.ts @@ -115,7 +115,6 @@ export const useAuthApiCall = () => { try { resp = await fetch(`${config.treUrl}/${endpoint}`, opts); } catch (err: any) { - console.error(err); let e = err as APIError; e.name = 'API call failure'; e.message = 'Unable to make call to API Backend';