Skip to content

Commit c05da12

Browse files
committed
do not show app links to users without workspace update access
1 parent bccde3f commit c05da12

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
@@ -26,6 +26,7 @@ export interface WorkspaceProps {
2626
resources?: TypesGen.WorkspaceResource[]
2727
getResourcesError?: Error
2828
builds?: TypesGen.WorkspaceBuild[]
29+
canUpdateWorkspace: boolean
2930
}
3031

3132
/**
@@ -42,6 +43,7 @@ export const Workspace: FC<WorkspaceProps> = ({
4243
resources,
4344
getResourcesError,
4445
builds,
46+
canUpdateWorkspace,
4547
}) => {
4648
const styles = useStyles()
4749

@@ -74,7 +76,12 @@ export const Workspace: FC<WorkspaceProps> = ({
7476

7577
<WorkspaceStats workspace={workspace} />
7678

77-
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
79+
<Resources
80+
resources={resources}
81+
getResourcesError={getResourcesError}
82+
workspace={workspace}
83+
canUpdateWorkspace={canUpdateWorkspace}
84+
/>
7885

7986
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
8087
<BuildsTable builds={builds} className={styles.timelineTable} />

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 { useNavigate, 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

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

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

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

@@ -57,6 +64,7 @@ export const WorkspacePage: React.FC = () => {
5764
resources={resources}
5865
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
5966
builds={builds}
67+
canUpdateWorkspace={canUpdateWorkspace}
6068
/>
6169
<DeleteWorkspaceDialog
6270
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({
@@ -487,14 +551,23 @@ export const workspaceMachine = createMachine(
487551
if (context.workspace) {
488552
return await API.getWorkspaceBuilds(context.workspace.id)
489553
} else {
490-
throw Error("Cannot refresh workspace without id")
554+
throw Error("Cannot get builds without id")
491555
}
492556
},
493557
loadMoreBuilds: async (context) => {
494558
if (context.workspace) {
495559
return await API.getWorkspaceBuilds(context.workspace.id)
496560
} else {
497-
throw Error("Cannot refresh workspace without id")
561+
throw Error("Cannot load more builds without id")
562+
}
563+
},
564+
checkPermissions: async (context) => {
565+
if (context.workspace && context.userId) {
566+
return await API.checkUserPermissions(context.userId, {
567+
checks: permissionsToCheck(context.workspace),
568+
})
569+
} else {
570+
throw Error("Cannot check permissions without both workspace and user id")
498571
}
499572
},
500573
},

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