Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"sessionId":"1b461965-840b-4e9c-933e-9870d26ecc24","pid":1743283,"procStart":"458332728","acquiredAt":1778812398765}
128 changes: 97 additions & 31 deletions react/src/components/ComputeSessionNodeItems/SessionActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,26 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({

const userInfo = useCurrentUserInfo();
const isOwner = userInfo[0]?.uuid === session?.user_id;
// The session row's access_key is set when the session was created.
// If the current keypair is different, manager APIs return 403
// ("Only admins can perform operations on behalf of other users.")
// — but only for the session's owner. Admins viewing another user's
// session (rendered here from SessionDetailContent) are allowed to act
// on behalf of that user, so the gate is owner-only.
const isAccessKeyMismatch =
!!session?.access_key &&
!!baiClient._config.accessKey &&
session.access_key !== baiClient._config.accessKey;
Comment thread
yomybaby marked this conversation as resolved.
const shouldDisableForMismatch = isOwner && isAccessKeyMismatch;

// Only swap to the mismatch tooltip when switching access keys would
// actually unblock the user. The button is also disabled when the
// session is inactive — in that case the "switch access key" advice
// is misleading, so fall back to the default tooltip.
const resolveTooltip = (defaultTitle: string) =>
shouldDisableForMismatch && isActive(session)
? t('session.AccessKeyMismatchTooltip')
: defaultTitle;

const hiddenButtons = React.useMemo(
() => new Set(hiddenButtonKeys ?? []),
Expand Down Expand Up @@ -181,26 +201,33 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
title={
isButtonTitleMode
? undefined
: t('session.ExecuteSpecificApp', {
appName: 'Jupyter Notebook',
})
: resolveTooltip(
t('session.ExecuteSpecificApp', {
appName: 'Jupyter Notebook',
}),
)
}
>
<Button
size={size}
type={'primary'}
disabled={
!isAppSupported(session) || !isActive(session) || !isOwner
!isAppSupported(session) ||
!isActive(session) ||
!isOwner ||
shouldDisableForMismatch
}
icon={<BAIJupyterIcon />}
onClick={() => {
launchApp();
}}
title={
isButtonTitleMode
? t('session.ExecuteSpecificApp', {
appName: 'Jupyter Notebook',
})
? resolveTooltip(
t('session.ExecuteSpecificApp', {
appName: 'Jupyter Notebook',
}),
)
: undefined
}
/>
Expand All @@ -211,26 +238,33 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
title={
isButtonTitleMode
? undefined
: t('session.ExecuteSpecificApp', {
appName: 'File browser',
})
: resolveTooltip(
t('session.ExecuteSpecificApp', {
appName: 'File browser',
}),
)
}
>
<Button
size={size}
type={'primary'}
disabled={
!isAppSupported(session) || !isActive(session) || !isOwner
!isAppSupported(session) ||
!isActive(session) ||
!isOwner ||
shouldDisableForMismatch
}
icon={<BAIFileBrowserIcon />}
onClick={() => {
launchApp();
}}
title={
isButtonTitleMode
? t('session.ExecuteSpecificApp', {
appName: 'File browser',
})
? resolveTooltip(
t('session.ExecuteSpecificApp', {
appName: 'File browser',
}),
)
: undefined
}
/>
Expand All @@ -241,31 +275,42 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
{isVisible('appLauncher') && (
<>
<Tooltip
title={isButtonTitleMode ? undefined : t('session.SeeAppDialog')}
title={
isButtonTitleMode
? undefined
: resolveTooltip(t('session.SeeAppDialog'))
}
>
<Button
size={size}
type={primaryAppOption ? undefined : 'primary'}
disabled={
!isAppSupported(session) || !isActive(session) || !isOwner
!isAppSupported(session) ||
!isActive(session) ||
!isOwner ||
shouldDisableForMismatch
}
icon={<BAIAppIcon />}
onClick={() => {
onAction?.('appLauncher');
setOpenAppLauncherModal(true);
}}
title={
isButtonTitleMode ? t('session.SeeAppDialog') : undefined
isButtonTitleMode
? resolveTooltip(t('session.SeeAppDialog'))
: undefined
}
/>
</Tooltip>
</>
)}
{isVisible('sftp') && (
<Tooltip title={t('data.explorer.RunSSH/SFTPserver')}>
<Tooltip title={resolveTooltip(t('data.explorer.RunSSH/SFTPserver'))}>
<Button
type="primary"
disabled={!isActive(session) || !isOwner}
disabled={
!isActive(session) || !isOwner || shouldDisableForMismatch
}
size={size}
icon={<BAISftpIcon />}
onClick={() => {
Expand All @@ -278,13 +323,18 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
<>
<Tooltip
title={
isButtonTitleMode ? undefined : t('session.ExecuteTerminalApp')
isButtonTitleMode
? undefined
: resolveTooltip(t('session.ExecuteTerminalApp'))
}
>
<Button
size={size}
disabled={
!isAppSupported(session) || !isActive(session) || !isOwner
!isAppSupported(session) ||
!isActive(session) ||
!isOwner ||
shouldDisableForMismatch
}
icon={<BAITerminalAppIcon />}
onClick={() => {
Expand All @@ -293,7 +343,7 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
}}
title={
isButtonTitleMode
? t('session.ExecuteTerminalApp')
? resolveTooltip(t('session.ExecuteTerminalApp'))
: undefined
}
/>
Expand All @@ -303,18 +353,23 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
{isVisible('logs') && (
<Tooltip
title={
isButtonTitleMode ? undefined : t('session.SeeContainerLogs')
isButtonTitleMode
? undefined
: resolveTooltip(t('session.SeeContainerLogs'))
}
>
<Button
size={size}
disabled={shouldDisableForMismatch}
icon={<BAISessionLogIcon />}
onClick={() => {
onAction?.('logs');
setOpenLogModal(true);
}}
title={
isButtonTitleMode ? t('session.SeeContainerLogs') : undefined
isButtonTitleMode
? resolveTooltip(t('session.SeeContainerLogs'))
: undefined
}
/>
</Tooltip>
Expand All @@ -324,20 +379,24 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
title={
isButtonTitleMode
? undefined
: t('session.RequestContainerCommit')
: resolveTooltip(t('session.RequestContainerCommit'))
}
>
<Button
size={size}
disabled={session?.status !== 'RUNNING' || !isOwner}
disabled={
session?.status !== 'RUNNING' ||
!isOwner ||
shouldDisableForMismatch
}
icon={<BAIContainerCommitIcon />}
onClick={() => {
onAction?.('containerCommit');
setOpenContainerCommitModal(true);
}}
title={
isButtonTitleMode
? t('session.RequestContainerCommit')
? resolveTooltip(t('session.RequestContainerCommit'))
: undefined
}
/>
Expand All @@ -346,16 +405,21 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
{isVisible('terminate') && (
<Tooltip
title={
isButtonTitleMode ? undefined : t('session.TerminateSession')
isButtonTitleMode
? undefined
: resolveTooltip(t('session.TerminateSession'))
}
>
<Button
size={size}
disabled={!isActive(session)}
disabled={!isActive(session) || shouldDisableForMismatch}
icon={
<BAITerminateIcon
style={{
color: isActive(session) ? token.colorError : undefined,
color:
isActive(session) && !shouldDisableForMismatch
? token.colorError
: undefined,
}}
/>
}
Expand All @@ -364,7 +428,9 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
setOpenTerminateModal(true);
}}
title={
isButtonTitleMode ? t('session.TerminateSession') : undefined
isButtonTitleMode
? resolveTooltip(t('session.TerminateSession'))
: undefined
}
/>
</Tooltip>
Expand Down
26 changes: 22 additions & 4 deletions react/src/components/SessionNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
type
service_ports
user_id
access_key
agent_ids
...SessionStatusTagFragment
...SessionReservationFragment
Expand Down Expand Up @@ -165,6 +166,19 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
session.type || '',
) && !_.isEmpty(JSON.parse(session.service_ports ?? '{}'));
const isOwner = userInfo?.uuid === session.user_id;
// 403 ("Only admins can perform operations on behalf of other
// users.") only hits owners on the wrong keypair. Admins acting
// on another user's session are permitted, so the gate is
// owner-only.
const shouldDisableForMismatch =
isOwner &&
!!session.access_key &&
!!baiClient._config.accessKey &&
session.access_key !== baiClient._config.accessKey;
const mismatchTitle =
shouldDisableForMismatch && isActive
? t('session.AccessKeyMismatchTooltip')
: undefined;
return (
<BAINameActionCell
title={name}
Expand All @@ -177,17 +191,21 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
actions={filterOutEmpty([
session.type !== 'system' && {
key: 'appLauncher',
title: t('session.SeeAppDialog'),
title: mismatchTitle ?? t('session.SeeAppDialog'),
icon: <BAIAppIcon />,
disabled: !isAppSupported || !isActive || !isOwner,
disabled:
!isAppSupported ||
!isActive ||
!isOwner ||
shouldDisableForMismatch,
onClick: () => setAppLauncherTarget(session),
},
{
key: 'terminate',
title: t('session.TerminateSession'),
title: mismatchTitle ?? t('session.TerminateSession'),
icon: <PowerOffIcon />,
type: 'danger' as const,
disabled: !isActive,
disabled: !isActive || shouldDisableForMismatch,
onClick: () => setTerminateTarget(session),
},
])}
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2487,6 +2487,7 @@
"PendingSessions": "Ausstehende Sitzungen"
},
"session": {
"AccessKeyMismatchTooltip": "Diese Sitzung wurde mit einem anderen Zugriffsschlüssel erstellt. Wechseln Sie zu diesem Zugriffsschlüssel, um diese Sitzung zu verwalten.",
"ActiveSessions": "Aktive Sitzungen",
"Agent": "Agent",
"AgentId": "Agenten -ID",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,7 @@
"PendingSessions": "Εκκρεμείς συνεδρίες"
},
"session": {
"AccessKeyMismatchTooltip": "Αυτή η συνεδρία δημιουργήθηκε με διαφορετικό κλειδί πρόσβασης. Μεταβείτε σε εκείνο το κλειδί πρόσβασης για να διαχειριστείτε αυτή τη συνεδρία.",
"ActiveSessions": "Ενεργές συνεδρίες",
"Agent": "Μέσο",
"AgentId": "Αναγνωριστικό πράκτορα",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2513,6 +2513,7 @@
"PendingSessions": "Pending Sessions"
},
"session": {
"AccessKeyMismatchTooltip": "This session was created under a different access key. Switch to that access key to manage this session.",
"ActiveSessions": "Active Sessions",
"Agent": "Agent",
"AgentId": "Agent ID",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,7 @@
"PendingSessions": "Sesiones pendientes"
},
"session": {
"AccessKeyMismatchTooltip": "Esta sesión se creó con una clave de acceso diferente. Cambia a esa clave de acceso para gestionar esta sesión.",
"ActiveSessions": "Sesiones activas",
"Agent": "Agente",
"AgentId": "ID de agente",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,7 @@
"PendingSessions": "Vireillä olevat istunnot"
},
"session": {
"AccessKeyMismatchTooltip": "Tämä istunto luotiin eri käyttöavaimella. Vaihda kyseiseen käyttöavaimeen hallitaksesi tätä istuntoa.",
"ActiveSessions": "Aktiiviset istunnot",
"Agent": "Agentti",
"AgentId": "Agenttitunnus",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2487,6 +2487,7 @@
"PendingSessions": "Séances en attente"
},
"session": {
"AccessKeyMismatchTooltip": "Cette session a été créée avec une clé d’accès différente. Passez à cette clé d’accès pour gérer cette session.",
"ActiveSessions": "Sessions actives",
"Agent": "Agent",
"AgentId": "ID d'agent",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -2488,6 +2488,7 @@
"PendingSessions": "Sesi yang tertunda"
},
"session": {
"AccessKeyMismatchTooltip": "Sesi ini dibuat dengan kunci akses yang berbeda. Beralihlah ke kunci akses tersebut untuk mengelola sesi ini.",
"ActiveSessions": "Sesi aktif",
"Agent": "Agen",
"AgentId": "ID Agen",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,7 @@
"PendingSessions": "Sessioni in sospeso"
},
"session": {
"AccessKeyMismatchTooltip": "Questa sessione è stata creata con una chiave di accesso diversa. Passa a quella chiave di accesso per gestire questa sessione.",
"ActiveSessions": "Sessioni attive",
"Agent": "Agente",
"AgentId": "ID agente",
Expand Down
Loading
Loading