Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc1b3080

Browse files
authored
fix: restrict edit schedule access (#2698)
1 parentea5c2cd commitc1b3080

File tree

7 files changed

+164
-29
lines changed

7 files changed

+164
-29
lines changed

‎site/src/components/Workspace/Workspace.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const Workspace: FC<WorkspaceProps> = ({
6464
workspace={workspace}
6565
onDeadlineMinus={scheduleProps.onDeadlineMinus}
6666
onDeadlinePlus={scheduleProps.onDeadlinePlus}
67+
canUpdateWorkspace={canUpdateWorkspace}
6768
/>
6869
<WorkspaceActions
6970
workspace={workspace}

‎site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const THIRTY = 30
1616
exportdefault{
1717
title:"components/WorkspaceSchedule",
1818
component:WorkspaceSchedule,
19+
argTypes:{
20+
canUpdateWorkspace:{
21+
defaultValue:true,
22+
},
23+
},
1924
}
2025

2126
constTemplate:Story<WorkspaceScheduleProps>=(args)=><WorkspaceSchedule{...args}/>
@@ -40,7 +45,7 @@ NoTTL.args = {
4045
...Mocks.MockWorkspace,
4146
latest_build:{
4247
...Mocks.MockWorkspaceBuild,
43-
// amannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
48+
// amanual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
4449
// SEE: #1834
4550
deadline:"0001-01-01T00:00:00Z",
4651
},
@@ -113,3 +118,17 @@ WorkspaceOffLong.args = {
113118
ttl_ms:2*365*24*60*60*1000,// 2 years
114119
},
115120
}
121+
122+
exportconstCannotEdit=Template.bind({})
123+
CannotEdit.args={
124+
workspace:{
125+
...Mocks.MockWorkspace,
126+
127+
latest_build:{
128+
...Mocks.MockWorkspaceBuild,
129+
transition:"stop",
130+
},
131+
ttl_ms:2*60*60*1000,// 2 hours
132+
},
133+
canUpdateWorkspace:false,
134+
}

‎site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ export const Language = {
3333

3434
exportinterfaceWorkspaceScheduleProps{
3535
workspace:Workspace
36+
canUpdateWorkspace:boolean
3637
}
3738

38-
exportconstWorkspaceSchedule:FC<WorkspaceScheduleProps>=({ workspace})=>{
39+
exportconstWorkspaceSchedule:FC<WorkspaceScheduleProps>=({
40+
workspace,
41+
canUpdateWorkspace,
42+
})=>{
3943
conststyles=useStyles()
4044
consttimezone=workspace.autostart_schedule
4145
?extractTimezone(workspace.autostart_schedule)
@@ -62,15 +66,17 @@ export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({ workspace }) =>
6266
</span>
6367
</Stack>
6468
</div>
65-
<div>
66-
<Link
67-
className={styles.scheduleAction}
68-
component={RouterLink}
69-
to={`/@${workspace.owner_name}/${workspace.name}/schedule`}
70-
>
71-
{Language.editScheduleLink}
72-
</Link>
73-
</div>
69+
{canUpdateWorkspace&&(
70+
<div>
71+
<Link
72+
className={styles.scheduleAction}
73+
component={RouterLink}
74+
to={`/@${workspace.owner_name}/${workspace.name}/schedule`}
75+
>
76+
{Language.editScheduleLink}
77+
</Link>
78+
</div>
79+
)}
7480
</Stack>
7581
</div>
7682
)

‎site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.stories.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const THIRTY = 30
1616
exportdefault{
1717
title:"components/WorkspaceScheduleButton",
1818
component:WorkspaceScheduleButton,
19+
argTypes:{
20+
canUpdateWorkspace:{
21+
defaultValue:true,
22+
},
23+
},
1924
}
2025

2126
constTemplate:Story<WorkspaceScheduleButtonProps>=(args)=>(
@@ -115,3 +120,17 @@ WorkspaceOffLong.args = {
115120
ttl_ms:2*365*24*60*60*1000,// 2 years
116121
},
117122
}
123+
124+
exportconstCannotEdit=Template.bind({})
125+
CannotEdit.args={
126+
workspace:{
127+
...Mocks.MockWorkspace,
128+
129+
latest_build:{
130+
...Mocks.MockWorkspaceBuild,
131+
transition:"stop",
132+
},
133+
ttl_ms:2*60*60*1000,// 2 hours
134+
},
135+
canUpdateWorkspace:false,
136+
}

‎site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ export interface WorkspaceScheduleButtonProps {
5454
workspace:Workspace
5555
onDeadlinePlus:()=>void
5656
onDeadlineMinus:()=>void
57+
canUpdateWorkspace:boolean
5758
}
5859

5960
exportconstWorkspaceScheduleButton:React.FC<WorkspaceScheduleButtonProps>=({
6061
workspace,
6162
onDeadlinePlus,
6263
onDeadlineMinus,
64+
canUpdateWorkspace,
6365
})=>{
6466
constanchorRef=useRef<HTMLButtonElement>(null)
6567
const[isOpen,setIsOpen]=useState(false)
@@ -74,7 +76,7 @@ export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = (
7476
<divclassName={styles.wrapper}>
7577
<divclassName={styles.label}>
7678
<WorkspaceScheduleLabelworkspace={workspace}/>
77-
{shouldDisplayPlusMinus(workspace)&&(
79+
{canUpdateWorkspace&&shouldDisplayPlusMinus(workspace)&&(
7880
<Stackdirection="row"spacing={0}>
7981
<IconButton
8082
className={styles.iconButton}
@@ -124,7 +126,7 @@ export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = (
124126
horizontal:"right",
125127
}}
126128
>
127-
<WorkspaceScheduleworkspace={workspace}/>
129+
<WorkspaceScheduleworkspace={workspace}canUpdateWorkspace={canUpdateWorkspace}/>
128130
</Popover>
129131
</div>
130132
</div>

‎site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import{useMachine}from"@xstate/react"
1+
import{useMachine,useSelector}from"@xstate/react"
22
import*ascronParserfrom"cron-parser"
33
importdayjsfrom"dayjs"
44
importtimezonefrom"dayjs/plugin/timezone"
55
importutcfrom"dayjs/plugin/utc"
6-
importReact,{useEffect}from"react"
6+
importReact,{useContext,useEffect}from"react"
77
import{useNavigate,useParams}from"react-router-dom"
88
import*asTypesGenfrom"../../api/typesGenerated"
99
import{ErrorSummary}from"../../components/ErrorSummary/ErrorSummary"
@@ -16,6 +16,8 @@ import {
1616
}from"../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
1717
import{firstOrItem}from"../../util/array"
1818
import{extractTimezone,stripTimezone}from"../../util/schedule"
19+
import{selectUser}from"../../xServices/auth/authSelectors"
20+
import{XServiceContext}from"../../xServices/StateContext"
1921
import{workspaceSchedule}from"../../xServices/workspaceSchedule/workspaceScheduleXService"
2022

2123
// REMARK: timezone plugin depends on UTC
@@ -24,6 +26,10 @@ import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceSc
2426
dayjs.extend(utc)
2527
dayjs.extend(timezone)
2628

29+
constLanguage={
30+
forbiddenError:"You don't have permissions to update the schedule for this workspace.",
31+
}
32+
2733
exportconstformValuesToAutoStartRequest=(
2834
values:WorkspaceScheduleFormValues,
2935
):TypesGen.UpdateWorkspaceAutostartRequest=>{
@@ -141,8 +147,17 @@ export const WorkspaceSchedulePage: React.FC = () => {
141147
constnavigate=useNavigate()
142148
constusername=firstOrItem(usernameQueryParam,null)
143149
constworkspaceName=firstOrItem(workspaceQueryParam,null)
144-
const[scheduleState,scheduleSend]=useMachine(workspaceSchedule)
145-
const{ formErrors, getWorkspaceError, workspace}=scheduleState.context
150+
151+
constxServices=useContext(XServiceContext)
152+
constme=useSelector(xServices.authXService,selectUser)
153+
154+
const[scheduleState,scheduleSend]=useMachine(workspaceSchedule,{
155+
context:{
156+
userId:me?.id,
157+
},
158+
})
159+
const{ checkPermissionsError, formErrors, getWorkspaceError, permissions, workspace}=
160+
scheduleState.context
146161

147162
// Get workspace on mount and whenever the args for getting a workspace change.
148163
// scheduleSend should not change.
@@ -153,20 +168,31 @@ export const WorkspaceSchedulePage: React.FC = () => {
153168
if(!username||!workspaceName){
154169
navigate("/workspaces")
155170
returnnull
156-
}elseif(
171+
}
172+
173+
if(
157174
scheduleState.matches("idle")||
158175
scheduleState.matches("gettingWorkspace")||
176+
scheduleState.matches("gettingPermissions")||
159177
!workspace
160178
){
161179
return<FullScreenLoader/>
162-
}elseif(scheduleState.matches("error")){
180+
}
181+
182+
if(scheduleState.matches("error")){
163183
return(
164184
<ErrorSummary
165-
error={getWorkspaceError}
185+
error={getWorkspaceError||checkPermissionsError}
166186
retry={()=>scheduleSend({type:"GET_WORKSPACE", username, workspaceName})}
167187
/>
168188
)
169-
}elseif(scheduleState.matches("presentForm")||scheduleState.matches("submittingSchedule")){
189+
}
190+
191+
if(!permissions?.updateWorkspace){
192+
return<ErrorSummaryerror={Error(Language.forbiddenError)}/>
193+
}
194+
195+
if(scheduleState.matches("presentForm")||scheduleState.matches("submittingSchedule")){
170196
return(
171197
<WorkspaceScheduleForm
172198
fieldErrors={formErrors}
@@ -184,13 +210,15 @@ export const WorkspaceSchedulePage: React.FC = () => {
184210
}}
185211
/>
186212
)
187-
}elseif(scheduleState.matches("submitSuccess")){
213+
}
214+
215+
if(scheduleState.matches("submitSuccess")){
188216
navigate(`/@${username}/${workspaceName}`)
189217
return<FullScreenLoader/>
190-
}else{
191-
// Theoretically impossible - log and bail
192-
console.error("WorkspaceSchedulePage: unknown state :: ",scheduleState)
193-
navigate("/")
194-
returnnull
195218
}
219+
220+
// Theoretically impossible - log and bail
221+
console.error("WorkspaceSchedulePage: unknown state :: ",scheduleState)
222+
navigate("/")
223+
returnnull
196224
}

‎site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export const Language = {
1414
successMessage:"Successfully updated workspace schedule.",
1515
}
1616

17+
typePermissions=Record<keyofReturnType<typeofpermissionsToCheck>,boolean>
18+
1719
exportinterfaceWorkspaceScheduleContext{
1820
formErrors?:FieldErrors
1921
getWorkspaceError?:Error|unknown
@@ -23,8 +25,27 @@ export interface WorkspaceScheduleContext {
2325
* machine is partially influenced by workspaceXService.
2426
*/
2527
workspace?:TypesGen.Workspace
28+
// permissions
29+
userId?:string
30+
permissions?:Permissions
31+
checkPermissionsError?:Error|unknown
2632
}
2733

34+
exportconstchecks={
35+
updateWorkspace:"updateWorkspace",
36+
}asconst
37+
38+
constpermissionsToCheck=(workspace:TypesGen.Workspace)=>({
39+
[checks.updateWorkspace]:{
40+
object:{
41+
resource_type:"workspace",
42+
resource_id:workspace.id,
43+
owner_id:workspace.owner_id,
44+
},
45+
action:"update",
46+
},
47+
})
48+
2849
exporttypeWorkspaceScheduleEvent=
2950
|{type:"GET_WORKSPACE";username:string;workspaceName:string}
3051
|{
@@ -60,7 +81,7 @@ export const workspaceSchedule = createMachine(
6081
src:"getWorkspace",
6182
id:"getWorkspace",
6283
onDone:{
63-
target:"presentForm",
84+
target:"gettingPermissions",
6485
actions:["assignWorkspace"],
6586
},
6687
onError:{
@@ -70,6 +91,25 @@ export const workspaceSchedule = createMachine(
7091
},
7192
tags:"loading",
7293
},
94+
gettingPermissions:{
95+
entry:"clearGetPermissionsError",
96+
invoke:{
97+
src:"checkPermissions",
98+
id:"checkPermissions",
99+
onDone:[
100+
{
101+
actions:["assignPermissions"],
102+
target:"presentForm",
103+
},
104+
],
105+
onError:[
106+
{
107+
actions:"assignGetPermissionsError",
108+
target:"error",
109+
},
110+
],
111+
},
112+
},
73113
presentForm:{
74114
on:{
75115
SUBMIT_SCHEDULE:"submittingSchedule",
@@ -113,8 +153,19 @@ export const workspaceSchedule = createMachine(
113153
assignGetWorkspaceError:assign({
114154
getWorkspaceError:(_,event)=>event.data,
115155
}),
156+
assignPermissions:assign({
157+
// Setting event.data as Permissions to be more stricted. So we know
158+
// what permissions we asked for.
159+
permissions:(_,event)=>event.dataasPermissions,
160+
}),
161+
assignGetPermissionsError:assign({
162+
checkPermissionsError:(_,event)=>event.data,
163+
}),
164+
clearGetPermissionsError:assign({
165+
checkPermissionsError:(_)=>undefined,
166+
}),
116167
clearContext:()=>{
117-
assign({workspace:undefined})
168+
assign({workspace:undefined,permissions:undefined})
118169
},
119170
clearGetWorkspaceError:(context)=>{
120171
assign({ ...context,getWorkspaceError:undefined})
@@ -134,6 +185,15 @@ export const workspaceSchedule = createMachine(
134185
getWorkspace:async(_,event)=>{
135186
returnawaitAPI.getWorkspaceByOwnerAndName(event.username,event.workspaceName)
136187
},
188+
checkPermissions:async(context)=>{
189+
if(context.workspace&&context.userId){
190+
returnawaitAPI.checkUserPermissions(context.userId,{
191+
checks:permissionsToCheck(context.workspace),
192+
})
193+
}else{
194+
throwError("Cannot check permissions without both workspace and user id")
195+
}
196+
},
137197
submitSchedule:async(context,event)=>{
138198
if(!context.workspace?.id){
139199
// This state is theoretically impossible, but helps TS

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp