Skip to content

Commit 46838e2

Browse files
committed
fix: scope gate to owners and extend to SessionNodes table
1 parent 5487b55 commit 46838e2

3 files changed

Lines changed: 44 additions & 19 deletions

File tree

.claude/scheduled_tasks.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"sessionId":"1b461965-840b-4e9c-933e-9870d26ecc24","pid":1743283,"procStart":"458332728","acquiredAt":1778812398765}

react/src/components/ComputeSessionNodeItems/SessionActionButtons.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,21 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
126126
// The session row's access_key is set when the session was created.
127127
// If the current keypair is different, manager APIs return 403
128128
// ("Only admins can perform operations on behalf of other users.")
129-
// for any per-session action — disable the buttons upfront instead.
129+
// — but only for the session's owner. Admins viewing another user's
130+
// session (rendered here from SessionDetailContent) are allowed to act
131+
// on behalf of that user, so the gate is owner-only.
130132
const isAccessKeyMismatch =
131133
!!session?.access_key &&
132134
!!baiClient._config.accessKey &&
133135
session.access_key !== baiClient._config.accessKey;
136+
const shouldDisableForMismatch = isOwner && isAccessKeyMismatch;
134137

135138
// Only swap to the mismatch tooltip when switching access keys would
136-
// actually unblock the user — i.e. they own the session. For non-owners
137-
// the button is already disabled for a different reason and the mismatch
138-
// copy ("Switch to that access key to manage this session") would be
139-
// misleading.
139+
// actually unblock the user. The button is also disabled when the
140+
// session is inactive — in that case the "switch access key" advice
141+
// is misleading, so fall back to the default tooltip.
140142
const resolveTooltip = (defaultTitle: string) =>
141-
isAccessKeyMismatch && isOwner
143+
shouldDisableForMismatch && isActive(session)
142144
? t('session.AccessKeyMismatchTooltip')
143145
: defaultTitle;
144146

@@ -213,7 +215,7 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
213215
!isAppSupported(session) ||
214216
!isActive(session) ||
215217
!isOwner ||
216-
isAccessKeyMismatch
218+
shouldDisableForMismatch
217219
}
218220
icon={<BAIJupyterIcon />}
219221
onClick={() => {
@@ -250,7 +252,7 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
250252
!isAppSupported(session) ||
251253
!isActive(session) ||
252254
!isOwner ||
253-
isAccessKeyMismatch
255+
shouldDisableForMismatch
254256
}
255257
icon={<BAIFileBrowserIcon />}
256258
onClick={() => {
@@ -286,7 +288,7 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
286288
!isAppSupported(session) ||
287289
!isActive(session) ||
288290
!isOwner ||
289-
isAccessKeyMismatch
291+
shouldDisableForMismatch
290292
}
291293
icon={<BAIAppIcon />}
292294
onClick={() => {
@@ -306,7 +308,9 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
306308
<Tooltip title={resolveTooltip(t('data.explorer.RunSSH/SFTPserver'))}>
307309
<Button
308310
type="primary"
309-
disabled={!isActive(session) || !isOwner || isAccessKeyMismatch}
311+
disabled={
312+
!isActive(session) || !isOwner || shouldDisableForMismatch
313+
}
310314
size={size}
311315
icon={<BAISftpIcon />}
312316
onClick={() => {
@@ -330,7 +334,7 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
330334
!isAppSupported(session) ||
331335
!isActive(session) ||
332336
!isOwner ||
333-
isAccessKeyMismatch
337+
shouldDisableForMismatch
334338
}
335339
icon={<BAITerminalAppIcon />}
336340
onClick={() => {
@@ -356,7 +360,7 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
356360
>
357361
<Button
358362
size={size}
359-
disabled={isAccessKeyMismatch}
363+
disabled={shouldDisableForMismatch}
360364
icon={<BAISessionLogIcon />}
361365
onClick={() => {
362366
onAction?.('logs');
@@ -381,7 +385,9 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
381385
<Button
382386
size={size}
383387
disabled={
384-
session?.status !== 'RUNNING' || !isOwner || isAccessKeyMismatch
388+
session?.status !== 'RUNNING' ||
389+
!isOwner ||
390+
shouldDisableForMismatch
385391
}
386392
icon={<BAIContainerCommitIcon />}
387393
onClick={() => {
@@ -406,12 +412,12 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
406412
>
407413
<Button
408414
size={size}
409-
disabled={!isActive(session) || isAccessKeyMismatch}
415+
disabled={!isActive(session) || shouldDisableForMismatch}
410416
icon={
411417
<BAITerminateIcon
412418
style={{
413419
color:
414-
isActive(session) && !isAccessKeyMismatch
420+
isActive(session) && !shouldDisableForMismatch
415421
? token.colorError
416422
: undefined,
417423
}}

react/src/components/SessionNodes.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
9696
type
9797
service_ports
9898
user_id
99+
access_key
99100
agent_ids
100101
...SessionStatusTagFragment
101102
...SessionReservationFragment
@@ -165,6 +166,19 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
165166
session.type || '',
166167
) && !_.isEmpty(JSON.parse(session.service_ports ?? '{}'));
167168
const isOwner = userInfo?.uuid === session.user_id;
169+
// 403 ("Only admins can perform operations on behalf of other
170+
// users.") only hits owners on the wrong keypair. Admins acting
171+
// on another user's session are permitted, so the gate is
172+
// owner-only.
173+
const shouldDisableForMismatch =
174+
isOwner &&
175+
!!session.access_key &&
176+
!!baiClient._config.accessKey &&
177+
session.access_key !== baiClient._config.accessKey;
178+
const mismatchTitle =
179+
shouldDisableForMismatch && isActive
180+
? t('session.AccessKeyMismatchTooltip')
181+
: undefined;
168182
return (
169183
<BAINameActionCell
170184
title={name}
@@ -177,17 +191,21 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
177191
actions={filterOutEmpty([
178192
session.type !== 'system' && {
179193
key: 'appLauncher',
180-
title: t('session.SeeAppDialog'),
194+
title: mismatchTitle ?? t('session.SeeAppDialog'),
181195
icon: <BAIAppIcon />,
182-
disabled: !isAppSupported || !isActive || !isOwner,
196+
disabled:
197+
!isAppSupported ||
198+
!isActive ||
199+
!isOwner ||
200+
shouldDisableForMismatch,
183201
onClick: () => setAppLauncherTarget(session),
184202
},
185203
{
186204
key: 'terminate',
187-
title: t('session.TerminateSession'),
205+
title: mismatchTitle ?? t('session.TerminateSession'),
188206
icon: <PowerOffIcon />,
189207
type: 'danger' as const,
190-
disabled: !isActive,
208+
disabled: !isActive || shouldDisableForMismatch,
191209
onClick: () => setTerminateTarget(session),
192210
},
193211
])}

0 commit comments

Comments
 (0)