Skip to content

Commit 158dd56

Browse files
committed
do not show app links to users without workspace update access
1 parent 700c108 commit 158dd56

File tree

6 files changed

+137
-35
lines changed

6 files changed

+137
-35
lines changed

site/src/components/Resources/Resources.tsx

+25-22
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ interface ResourcesProps {
6363
resources?: WorkspaceResource[]
6464
getResourcesError?: Error
6565
workspace: Workspace
66+
canUpdateWorkspace: boolean
6667
}
6768

68-
export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, workspace }) => {
69+
export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, workspace, canUpdateWorkspace }) => {
6970
const styles = useStyles()
7071
const theme: Theme = useTheme()
7172

@@ -89,7 +90,7 @@ export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, wo
8990
<AgentHelpTooltip />
9091
</Stack>
9192
</TableCell>
92-
<TableCell>{Language.accessLabel}</TableCell>
93+
{canUpdateWorkspace && <TableCell>{Language.accessLabel}</TableCell>}
9394
<TableCell>{Language.statusLabel}</TableCell>
9495
</TableHeaderRow>
9596
</TableHead>
@@ -130,28 +131,30 @@ export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, wo
130131
{agent.name}
131132
<span className={styles.operatingSystem}>{agent.operating_system}</span>
132133
</TableCell>
133-
<TableCell>
134-
<Stack>
135-
{agent.status === "connected" && (
136-
<TerminalLink
137-
className={styles.accessLink}
138-
workspaceName={workspace.name}
139-
agentName={agent.name}
140-
userName={workspace.owner_name}
141-
/>
142-
)}
143-
{agent.status === "connected" &&
144-
agent.apps.map((app) => (
145-
<AppLink
146-
key={app.name}
147-
appIcon={app.icon}
148-
appName={app.name}
149-
userName={workspace.owner_name}
134+
{canUpdateWorkspace && (
135+
<TableCell>
136+
<Stack>
137+
{agent.status === "connected" && (
138+
<TerminalLink
139+
className={styles.accessLink}
150140
workspaceName={workspace.name}
141+
agentName={agent.name}
142+
userName={workspace.owner_name}
151143
/>
152-
))}
153-
</Stack>
154-
</TableCell>
144+
)}
145+
{agent.status === "connected" &&
146+
agent.apps.map((app) => (
147+
<AppLink
148+
key={app.name}
149+
appIcon={app.icon}
150+
appName={app.name}
151+
userName={workspace.owner_name}
152+
workspaceName={workspace.name}
153+
/>
154+
))}
155+
</Stack>
156+
</TableCell>
157+
)}
155158
<TableCell>
156159
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
157160
{getDisplayAgentStatus(theme, agent).status}

site/src/components/Workspace/Workspace.stories.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ Started.args = {
2222
handleStop: action("stop"),
2323
resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
2424
builds: [Mocks.MockWorkspaceBuild],
25+
canUpdateWorkspace: true,
26+
}
27+
28+
export const WithoutUpdateAccess = Template.bind({})
29+
WithoutUpdateAccess.args = {
30+
...Started.args,
31+
canUpdateWorkspace: false,
2532
}
2633

2734
export const Starting = Template.bind({})

site/src/components/Workspace/Workspace.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface WorkspaceProps {
2828
resources?: TypesGen.WorkspaceResource[]
2929
getResourcesError?: Error
3030
builds?: TypesGen.WorkspaceBuild[]
31+
canUpdateWorkspace: boolean
3132
}
3233

3334
/**
@@ -44,6 +45,7 @@ export const Workspace: FC<WorkspaceProps> = ({
4445
resources,
4546
getResourcesError,
4647
builds,
48+
canUpdateWorkspace,
4749
}) => {
4850
const styles = useStyles()
4951
const navigate = useNavigate()
@@ -80,7 +82,12 @@ export const Workspace: FC<WorkspaceProps> = ({
8082
<WorkspaceStats workspace={workspace} />
8183

8284
{!!resources && !!resources.length && (
83-
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
85+
<Resources
86+
resources={resources}
87+
getResourcesError={getResourcesError}
88+
workspace={workspace}
89+
canUpdateWorkspace={canUpdateWorkspace}
90+
/>
8491
)}
8592

8693
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>

site/src/pages/WorkspacePage/WorkspacePage.tsx

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useMachine } from "@xstate/react"
2-
import React, { useEffect } from "react"
1+
import { useMachine, useSelector } from "@xstate/react"
2+
import React, { useContext, useEffect } from "react"
33
import { Helmet } from "react-helmet"
44
import { useParams } from "react-router-dom"
55
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
@@ -8,6 +8,8 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
88
import { Workspace } from "../../components/Workspace/Workspace"
99
import { firstOrItem } from "../../util/array"
1010
import { pageTitle } from "../../util/page"
11+
import { selectUser } from "../../xServices/auth/authSelectors"
12+
import { XServiceContext } from "../../xServices/StateContext"
1113
import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
1214
import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService"
1315

@@ -16,8 +18,13 @@ export const WorkspacePage: React.FC = () => {
1618
const username = firstOrItem(usernameQueryParam, null)
1719
const workspaceName = firstOrItem(workspaceQueryParam, null)
1820

19-
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
20-
const { workspace, resources, getWorkspaceError, getResourcesError, builds } = workspaceState.context
21+
const xServices = useContext(XServiceContext)
22+
const me = useSelector(xServices.authXService, selectUser)
23+
24+
const [workspaceState, workspaceSend] = useMachine(workspaceMachine.withContext({ userId: me?.id }))
25+
const { workspace, resources, getWorkspaceError, getResourcesError, builds, permissions } = workspaceState.context
26+
27+
const canUpdateWorkspace = !!permissions?.updateWorkspace
2128

2229
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
2330

@@ -56,6 +63,7 @@ export const WorkspacePage: React.FC = () => {
5663
resources={resources}
5764
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
5865
builds={builds}
66+
canUpdateWorkspace={canUpdateWorkspace}
5967
/>
6068
<DeleteWorkspaceDialog
6169
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}

site/src/xServices/auth/authSelectors.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ export const selectOrgId = (state: AuthState): string | undefined => {
1010
export const selectPermissions = (state: AuthState): AuthContext["permissions"] => {
1111
return state.context.permissions
1212
}
13+
14+
export const selectUser = (state: AuthState): AuthContext["me"] => {
15+
return state.context.me
16+
}

site/src/xServices/workspace/workspaceXService.ts

+81-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const Language = {
1717
buildError: "Workspace action failed.",
1818
}
1919

20+
type Permissions = Record<keyof ReturnType<typeof permissionsToCheck>, boolean>
21+
2022
export interface WorkspaceContext {
2123
workspace?: TypesGen.Workspace
2224
template?: TypesGen.Template
@@ -26,14 +28,18 @@ export interface WorkspaceContext {
2628
// error creating a new WorkspaceBuild
2729
buildError?: Error | unknown
2830
// these are separate from getX errors because they don't make the page unusable
29-
refreshWorkspaceError: Error | unknown
30-
refreshTemplateError: Error | unknown
31-
getResourcesError: Error | unknown
31+
refreshWorkspaceError?: Error | unknown
32+
refreshTemplateError?: Error | unknown
33+
getResourcesError?: Error | unknown
3234
// Builds
3335
builds?: TypesGen.WorkspaceBuild[]
3436
getBuildsError?: Error | unknown
3537
loadMoreBuildsError?: Error | unknown
36-
cancellationMessage: string
38+
cancellationMessage?: string
39+
// permissions
40+
permissions?: Permissions
41+
checkPermissionsError?: Error | unknown
42+
userId?: string
3743
}
3844

3945
export type WorkspaceEvent =
@@ -48,6 +54,30 @@ export type WorkspaceEvent =
4854
| { type: "LOAD_MORE_BUILDS" }
4955
| { type: "REFRESH_TIMELINE" }
5056

57+
export const checks = {
58+
readWorkspace: "readWorkspace",
59+
updateWorkspace: "updateWorkspace",
60+
} as const
61+
62+
const permissionsToCheck = (workspace: TypesGen.Workspace) => ({
63+
[checks.readWorkspace]: {
64+
object: {
65+
resource_type: "workspace",
66+
resource_id: workspace.id,
67+
owner_id: workspace.owner_id,
68+
},
69+
action: "read",
70+
},
71+
[checks.updateWorkspace]: {
72+
object: {
73+
resource_type: "workspace",
74+
resource_id: workspace.id,
75+
owner_id: workspace.owner_id,
76+
},
77+
action: "update",
78+
},
79+
})
80+
5181
export const workspaceMachine = createMachine(
5282
{
5383
tsTypes: {} as import("./workspaceXService.typegen").Typegen0,
@@ -82,6 +112,9 @@ export const workspaceMachine = createMachine(
82112
loadMoreBuilds: {
83113
data: TypesGen.WorkspaceBuild[]
84114
}
115+
checkPermissions: {
116+
data: TypesGen.UserAuthorizationResponse
117+
}
85118
},
86119
},
87120
id: "workspaceState",
@@ -99,7 +132,7 @@ export const workspaceMachine = createMachine(
99132
src: "getWorkspace",
100133
id: "getWorkspace",
101134
onDone: {
102-
target: "ready",
135+
target: "gettingPermissions",
103136
actions: ["assignWorkspace"],
104137
},
105138
onError: {
@@ -109,6 +142,25 @@ export const workspaceMachine = createMachine(
109142
},
110143
tags: "loading",
111144
},
145+
gettingPermissions: {
146+
entry: "clearGetPermissionsError",
147+
invoke: {
148+
src: "checkPermissions",
149+
id: "checkPermissions",
150+
onDone: [
151+
{
152+
actions: ["assignPermissions"],
153+
target: "ready",
154+
},
155+
],
156+
onError: [
157+
{
158+
actions: "assignGetPermissionsError",
159+
target: "error",
160+
},
161+
],
162+
},
163+
},
112164
ready: {
113165
type: "parallel",
114166
states: {
@@ -312,6 +364,7 @@ export const workspaceMachine = createMachine(
312364
workspace: undefined,
313365
template: undefined,
314366
build: undefined,
367+
permissions: undefined,
315368
}),
316369
assignWorkspace: assign({
317370
workspace: (_, event) => event.data,
@@ -323,6 +376,17 @@ export const workspaceMachine = createMachine(
323376
assignTemplate: assign({
324377
template: (_, event) => event.data,
325378
}),
379+
assignPermissions: assign({
380+
// Setting event.data as Permissions to be more stricted. So we know
381+
// what permissions we asked for.
382+
permissions: (_, event) => event.data as Permissions,
383+
}),
384+
assignGetPermissionsError: assign({
385+
checkPermissionsError: (_, event) => event.data,
386+
}),
387+
clearGetPermissionsError: assign({
388+
checkPermissionsError: (_) => undefined,
389+
}),
326390
assignBuild: (_, event) =>
327391
assign({
328392
build: event.data,
@@ -347,7 +411,7 @@ export const workspaceMachine = createMachine(
347411
cancellationMessage: undefined,
348412
}),
349413
displayCancellationError: (context) => {
350-
displayError(context.cancellationMessage)
414+
displayError(context.cancellationMessage || "Cancellation failed")
351415
},
352416
assignRefreshWorkspaceError: (_, event) =>
353417
assign({
@@ -489,14 +553,23 @@ export const workspaceMachine = createMachine(
489553
if (context.workspace) {
490554
return await API.getWorkspaceBuilds(context.workspace.id)
491555
} else {
492-
throw Error("Cannot refresh workspace without id")
556+
throw Error("Cannot get builds without id")
493557
}
494558
},
495559
loadMoreBuilds: async (context) => {
496560
if (context.workspace) {
497561
return await API.getWorkspaceBuilds(context.workspace.id)
498562
} else {
499-
throw Error("Cannot refresh workspace without id")
563+
throw Error("Cannot load more builds without id")
564+
}
565+
},
566+
checkPermissions: async (context) => {
567+
if (context.workspace && context.userId) {
568+
return await API.checkUserPermissions(context.userId, {
569+
checks: permissionsToCheck(context.workspace),
570+
})
571+
} else {
572+
throw Error("Cannot check permissions without both workspace and user id")
500573
}
501574
},
502575
},

0 commit comments

Comments
 (0)