From f54127347b637a4be228fbf893aace4b48ca345c Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Tue, 28 Jun 2022 07:10:16 +0000 Subject: [PATCH 1/6] fix: restrict edit schedule access --- site/src/components/Workspace/Workspace.tsx | 1 + .../WorkspaceSchedule.stories.tsx | 21 +++++- .../WorkspaceSchedule/WorkspaceSchedule.tsx | 23 ++++--- .../WorkspaceScheduleButton.tsx | 6 +- .../WorkspaceSchedulePage.tsx | 28 ++++++-- .../workspaceScheduleXService.ts | 64 ++++++++++++++++++- 6 files changed, 123 insertions(+), 20 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index e1f706d3fcfc9..8c0ab305481fc 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -64,6 +64,7 @@ export const Workspace: FC = ({ workspace={workspace} onDeadlineMinus={scheduleProps.onDeadlineMinus} onDeadlinePlus={scheduleProps.onDeadlinePlus} + canUpdateWorkspace={canUpdateWorkspace} /> = (args) => @@ -40,7 +45,7 @@ NoTTL.args = { ...Mocks.MockWorkspace, latest_build: { ...Mocks.MockWorkspaceBuild, - // a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"' + // a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"' // SEE: #1834 deadline: "0001-01-01T00:00:00Z", }, @@ -113,3 +118,17 @@ WorkspaceOffLong.args = { ttl_ms: 2 * 365 * 24 * 60 * 60 * 1000, // 2 years }, } + +export const CannotEdit = Template.bind({}) +CannotEdit.args = { + workspace: { + ...Mocks.MockWorkspace, + + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "stop", + }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours + }, + canUpdateWorkspace: false, +} diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx index b6730733fe784..67f80bac12e5c 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx +++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx @@ -33,9 +33,10 @@ export const Language = { export interface WorkspaceScheduleProps { workspace: Workspace + canUpdateWorkspace: boolean } -export const WorkspaceSchedule: FC = ({ workspace }) => { +export const WorkspaceSchedule: FC = ({ workspace, canUpdateWorkspace }) => { const styles = useStyles() const timezone = workspace.autostart_schedule ? extractTimezone(workspace.autostart_schedule) @@ -62,15 +63,17 @@ export const WorkspaceSchedule: FC = ({ workspace }) => -
- - {Language.editScheduleLink} - -
+ {canUpdateWorkspace && ( +
+ + {Language.editScheduleLink} + +
+ )} ) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx index 37dff6205e7ae..d03c0ad357136 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx @@ -54,12 +54,14 @@ export interface WorkspaceScheduleButtonProps { workspace: Workspace onDeadlinePlus: () => void onDeadlineMinus: () => void + canUpdateWorkspace: boolean } export const WorkspaceScheduleButton: React.FC = ({ workspace, onDeadlinePlus, onDeadlineMinus, + canUpdateWorkspace, }) => { const anchorRef = useRef(null) const [isOpen, setIsOpen] = useState(false) @@ -74,7 +76,7 @@ export const WorkspaceScheduleButton: React.FC = (
- {shouldDisplayPlusMinus(workspace) && ( + {canUpdateWorkspace && shouldDisplayPlusMinus(workspace) && ( = ( horizontal: "right", }} > - +
diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 5aa9da8527a1e..89fe012fd4b96 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -1,9 +1,9 @@ -import { useMachine } from "@xstate/react" +import { useMachine, useSelector } from "@xstate/react" import * as cronParser from "cron-parser" import dayjs from "dayjs" import timezone from "dayjs/plugin/timezone" import utc from "dayjs/plugin/utc" -import React, { useEffect } from "react" +import React, { useContext, useEffect } from "react" import { useNavigate, useParams } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" @@ -16,6 +16,8 @@ import { } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm" import { firstOrItem } from "../../util/array" import { extractTimezone, stripTimezone } from "../../util/schedule" +import { selectUser } from "../../xServices/auth/authSelectors" +import { XServiceContext } from "../../xServices/StateContext" import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceScheduleXService" // REMARK: timezone plugin depends on UTC @@ -24,6 +26,10 @@ import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceSc dayjs.extend(utc) dayjs.extend(timezone) +const Language = { + forbiddenError: "403: Workspace schedule update forbidden.", +} + export const formValuesToAutoStartRequest = ( values: WorkspaceScheduleFormValues, ): TypesGen.UpdateWorkspaceAutostartRequest => { @@ -141,8 +147,17 @@ export const WorkspaceSchedulePage: React.FC = () => { const navigate = useNavigate() const username = firstOrItem(usernameQueryParam, null) const workspaceName = firstOrItem(workspaceQueryParam, null) - const [scheduleState, scheduleSend] = useMachine(workspaceSchedule) - const { formErrors, getWorkspaceError, workspace } = scheduleState.context + + const xServices = useContext(XServiceContext) + const me = useSelector(xServices.authXService, selectUser) + + const [scheduleState, scheduleSend] = useMachine(workspaceSchedule, { + context: { + userId: me?.id, + }, + }) + const { checkPermissionsError, formErrors, getWorkspaceError, permissions, workspace } = + scheduleState.context // Get workspace on mount and whenever the args for getting a workspace change. // scheduleSend should not change. @@ -156,16 +171,19 @@ export const WorkspaceSchedulePage: React.FC = () => { } else if ( scheduleState.matches("idle") || scheduleState.matches("gettingWorkspace") || + scheduleState.matches("gettingPermissions") || !workspace ) { return } else if (scheduleState.matches("error")) { return ( scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })} /> ) + } else if (!permissions?.updateWorkspace) { + return } else if (scheduleState.matches("presentForm") || scheduleState.matches("submittingSchedule")) { return ( , boolean> + export interface WorkspaceScheduleContext { formErrors?: FieldErrors getWorkspaceError?: Error | unknown @@ -23,8 +25,27 @@ export interface WorkspaceScheduleContext { * machine is partially influenced by workspaceXService. */ workspace?: TypesGen.Workspace + // permissions + userId?: string + permissions?: Permissions + checkPermissionsError?: Error | unknown } +export const checks = { + updateWorkspace: "updateWorkspace", +} as const + +const permissionsToCheck = (workspace: TypesGen.Workspace) => ({ + [checks.updateWorkspace]: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "update", + }, +}) + export type WorkspaceScheduleEvent = | { type: "GET_WORKSPACE"; username: string; workspaceName: string } | { @@ -60,7 +81,7 @@ export const workspaceSchedule = createMachine( src: "getWorkspace", id: "getWorkspace", onDone: { - target: "presentForm", + target: "gettingPermissions", actions: ["assignWorkspace"], }, onError: { @@ -70,6 +91,25 @@ export const workspaceSchedule = createMachine( }, tags: "loading", }, + gettingPermissions: { + entry: "clearGetPermissionsError", + invoke: { + src: "checkPermissions", + id: "checkPermissions", + onDone: [ + { + actions: ["assignPermissions"], + target: "presentForm", + }, + ], + onError: [ + { + actions: "assignGetPermissionsError", + target: "error", + }, + ], + }, + }, presentForm: { on: { SUBMIT_SCHEDULE: "submittingSchedule", @@ -113,8 +153,19 @@ export const workspaceSchedule = createMachine( assignGetWorkspaceError: assign({ getWorkspaceError: (_, event) => event.data, }), + assignPermissions: assign({ + // Setting event.data as Permissions to be more stricted. So we know + // what permissions we asked for. + permissions: (_, event) => event.data as Permissions, + }), + assignGetPermissionsError: assign({ + checkPermissionsError: (_, event) => event.data, + }), + clearGetPermissionsError: assign({ + checkPermissionsError: (_) => undefined, + }), clearContext: () => { - assign({ workspace: undefined }) + assign({ workspace: undefined, permissions: undefined }) }, clearGetWorkspaceError: (context) => { assign({ ...context, getWorkspaceError: undefined }) @@ -134,6 +185,15 @@ export const workspaceSchedule = createMachine( getWorkspace: async (_, event) => { return await API.getWorkspaceByOwnerAndName(event.username, event.workspaceName) }, + checkPermissions: async (context) => { + if (context.workspace && context.userId) { + return await API.checkUserPermissions(context.userId, { + checks: permissionsToCheck(context.workspace), + }) + } else { + throw Error("Cannot check permissions without both workspace and user id") + } + }, submitSchedule: async (context, event) => { if (!context.workspace?.id) { // This state is theoretically impossible, but helps TS From 26ba36d13f4731a415af6377e8f3eaad55c10762 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Tue, 28 Jun 2022 23:07:05 +0000 Subject: [PATCH 2/6] refactor workspace schedule page code --- .../WorkspaceSchedulePage.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 89fe012fd4b96..104fa45969e5a 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -168,23 +168,31 @@ export const WorkspaceSchedulePage: React.FC = () => { if (!username || !workspaceName) { navigate("/workspaces") return null - } else if ( + } + + if ( scheduleState.matches("idle") || scheduleState.matches("gettingWorkspace") || scheduleState.matches("gettingPermissions") || !workspace ) { return - } else if (scheduleState.matches("error")) { + } + + if (scheduleState.matches("error")) { return ( scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })} /> ) - } else if (!permissions?.updateWorkspace) { + } + + if (!permissions?.updateWorkspace) { return - } else if (scheduleState.matches("presentForm") || scheduleState.matches("submittingSchedule")) { + } + + if (scheduleState.matches("presentForm") || scheduleState.matches("submittingSchedule")) { return ( { }} /> ) - } else if (scheduleState.matches("submitSuccess")) { + } + + if (scheduleState.matches("submitSuccess")) { navigate(`/@${username}/${workspaceName}`) return - } else { - // Theoretically impossible - log and bail - console.error("WorkspaceSchedulePage: unknown state :: ", scheduleState) - navigate("/") - return null } + + // Theoretically impossible - log and bail + console.error("WorkspaceSchedulePage: unknown state :: ", scheduleState) + navigate("/") + return null } From fd7b922b906812062265cb8d26649b33b86ca275 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 1 Jul 2022 08:48:10 +0000 Subject: [PATCH 3/6] update error message --- site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 104fa45969e5a..fca16071350b3 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -27,7 +27,7 @@ dayjs.extend(utc) dayjs.extend(timezone) const Language = { - forbiddenError: "403: Workspace schedule update forbidden.", + forbiddenError: "You don't have permissions to update the schedule for this workspace.", } export const formValuesToAutoStartRequest = ( From 2233bdc3003beb3a5daffe9a419a8e1327c02a3f Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 1 Jul 2022 19:35:36 +0000 Subject: [PATCH 4/6] add story for schedule button --- .../WorkspaceScheduleButton.stories.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx index ec5a54fd17940..3b058a4e69f9a 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx @@ -115,3 +115,17 @@ WorkspaceOffLong.args = { ttl_ms: 2 * 365 * 24 * 60 * 60 * 1000, // 2 years }, } + +export const CannotEdit = Template.bind({}) +CannotEdit.args = { + workspace: { + ...Mocks.MockWorkspace, + + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "stop", + }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours + }, + canUpdateWorkspace: false, +} From d08025dfb1b6c4bb33393fb9e0aa7457b9a55718 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 1 Jul 2022 19:39:43 +0000 Subject: [PATCH 5/6] fix lint --- site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx index 67f80bac12e5c..8878f1b33b072 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx +++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx @@ -36,7 +36,10 @@ export interface WorkspaceScheduleProps { canUpdateWorkspace: boolean } -export const WorkspaceSchedule: FC = ({ workspace, canUpdateWorkspace }) => { +export const WorkspaceSchedule: FC = ({ + workspace, + canUpdateWorkspace, +}) => { const styles = useStyles() const timezone = workspace.autostart_schedule ? extractTimezone(workspace.autostart_schedule) From 2d2522df6b1aa1a019c22149681b3ac6b759679e Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 1 Jul 2022 19:52:42 +0000 Subject: [PATCH 6/6] fix schedule button stories --- .../WorkspaceScheduleButton.stories.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx index 3b058a4e69f9a..32ff5668783ce 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx @@ -16,6 +16,11 @@ const THIRTY = 30 export default { title: "components/WorkspaceScheduleButton", component: WorkspaceScheduleButton, + argTypes: { + canUpdateWorkspace: { + defaultValue: true, + }, + }, } const Template: Story = (args) => ( 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