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

Lines changed: 25 additions & 22 deletions
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 8 additions & 1 deletion
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

Lines changed: 12 additions & 4 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 81 additions & 8 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy