Skip to content
Merged
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
16 changes: 6 additions & 10 deletions src/crates/core/src/service/remote_ssh/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,11 @@ impl SSHConnectionManager {
username: config.username.clone(),
auth_type: match &config.auth {
SSHAuthMethod::Password { .. } => crate::service::remote_ssh::types::SavedAuthType::Password,
SSHAuthMethod::PrivateKey { key_path, .. } => crate::service::remote_ssh::types::SavedAuthType::PrivateKey { key_path: key_path.clone() },
SSHAuthMethod::Agent => crate::service::remote_ssh::types::SavedAuthType::Agent,
SSHAuthMethod::PrivateKey { key_path, .. } => {
crate::service::remote_ssh::types::SavedAuthType::PrivateKey {
key_path: key_path.clone(),
}
}
},
default_workspace: config.default_workspace.clone(),
last_connected: Some(chrono::Utc::now().timestamp() as u64),
Expand All @@ -722,7 +725,7 @@ impl SSHConnectionManager {
.with_context(|| format!("store ssh password vault for {}", config.id))?;
}
}
SSHAuthMethod::PrivateKey { .. } | SSHAuthMethod::Agent => {
SSHAuthMethod::PrivateKey { .. } => {
self.password_vault.remove(&config.id).await?;
}
}
Expand Down Expand Up @@ -816,7 +819,6 @@ impl SSHConnectionManager {
log::info!("Successfully decoded private key");
Some(key_pair)
}
SSHAuthMethod::Agent => None,
};

let ssh_config = Arc::new(russh::client::Config {
Expand Down Expand Up @@ -927,12 +929,6 @@ impl SSHConnectionManager {
return Err(anyhow!("Failed to load private key"));
}
}
SSHAuthMethod::Agent => {
log::debug!("Using SSH agent authentication - agent auth not supported, returning false");
// Agent auth is not supported in russh - return false to indicate auth failed
// The caller should try another auth method
false
}
};

if !auth_success {
Expand Down
65 changes: 60 additions & 5 deletions src/crates/core/src/service/remote_ssh/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Type definitions for Remote SSH service

use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};

/// Workspace backend type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
Expand Down Expand Up @@ -39,6 +39,7 @@ pub struct SSHConnectionConfig {
/// SSH username
pub username: String,
/// Authentication method
#[serde(deserialize_with = "deserialize_ssh_auth_method")]
pub auth: SSHAuthMethod,
/// Default remote working directory
#[serde(rename = "defaultWorkspace")]
Expand All @@ -61,8 +62,40 @@ pub enum SSHAuthMethod {
/// Optional passphrase for encrypted private key
passphrase: Option<String>,
},
/// SSH agent authentication (uses system SSH agent)
Agent,
}

/// Legacy `{"type":"Agent"}` in saved config maps to default private key path.
fn deserialize_ssh_auth_method<'de, D>(deserializer: D) -> Result<SSHAuthMethod, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(tag = "type")]
enum Helper {
Password {
password: String,
},
PrivateKey {
#[serde(rename = "keyPath")]
key_path: String,
passphrase: Option<String>,
},
Agent,
}
match Helper::deserialize(deserializer)? {
Helper::Password { password } => Ok(SSHAuthMethod::Password { password }),
Helper::PrivateKey {
key_path,
passphrase,
} => Ok(SSHAuthMethod::PrivateKey {
key_path,
passphrase,
}),
Helper::Agent => Ok(SSHAuthMethod::PrivateKey {
key_path: "~/.ssh/id_rsa".to_string(),
passphrase: None,
}),
}
}

/// Connection state
Expand All @@ -87,7 +120,7 @@ pub struct SavedConnection {
pub host: String,
pub port: u16,
pub username: String,
#[serde(rename = "authType")]
#[serde(rename = "authType", deserialize_with = "deserialize_saved_auth_type")]
pub auth_type: SavedAuthType,
#[serde(rename = "defaultWorkspace")]
pub default_workspace: Option<String>,
Expand All @@ -104,7 +137,29 @@ pub enum SavedAuthType {
#[serde(rename = "keyPath")]
key_path: String,
},
Agent,
}

fn deserialize_saved_auth_type<'de, D>(deserializer: D) -> Result<SavedAuthType, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(tag = "type")]
enum Helper {
Password,
PrivateKey {
#[serde(rename = "keyPath")]
key_path: String,
},
Agent,
}
match Helper::deserialize(deserializer)? {
Helper::Password => Ok(SavedAuthType::Password),
Helper::PrivateKey { key_path } => Ok(SavedAuthType::PrivateKey { key_path }),
Helper::Agent => Ok(SavedAuthType::PrivateKey {
key_path: "~/.ssh/id_rsa".to_string(),
}),
}
}

/// Remote file entry information
Expand Down
22 changes: 7 additions & 15 deletions src/web-ui/src/features/ssh-remote/SSHAuthPromptDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Unified SSH authentication prompt: password, private key, or SSH agent.
* Unified SSH authentication prompt: password or private key.
*/

import React, { useState, useEffect, useRef, useCallback } from 'react';
Expand All @@ -9,7 +9,7 @@ import { Button } from '@/component-library';
import { Input } from '@/component-library';
import { Select } from '@/component-library';
import { IconButton } from '@/component-library';
import { FolderOpen, Key, Loader2, Lock, Server, Terminal, User } from 'lucide-react';
import { FolderOpen, Key, Loader2, Lock, Server, User } from 'lucide-react';
import type { SSHAuthMethod } from './types';
import { pickSshPrivateKeyPath } from './pickSshPrivateKeyPath';
import './SSHAuthPromptDialog.scss';
Expand All @@ -24,7 +24,7 @@ interface SSHAuthPromptDialogProps {
open: boolean;
/** Shown in the header area (e.g. user@host:port or alias) */
targetDescription: string;
defaultAuthMethod: 'password' | 'privateKey' | 'agent';
defaultAuthMethod: 'password' | 'privateKey';
defaultKeyPath?: string;
initialUsername: string;
/** If false, user can edit username (e.g. SSH config without User) */
Expand All @@ -46,7 +46,7 @@ export const SSHAuthPromptDialog: React.FC<SSHAuthPromptDialogProps> = ({
onCancel,
}) => {
const { t } = useI18n('common');
const [authMethod, setAuthMethod] = useState<'password' | 'privateKey' | 'agent'>(defaultAuthMethod);
const [authMethod, setAuthMethod] = useState<'password' | 'privateKey'>(defaultAuthMethod);
const [username, setUsername] = useState(initialUsername);
const [password, setPassword] = useState('');
const [keyPath, setKeyPath] = useState(defaultKeyPath);
Expand All @@ -71,15 +71,13 @@ export const SSHAuthPromptDialog: React.FC<SSHAuthPromptDialogProps> = ({
const authOptions = [
{ label: t('ssh.remote.password') || 'Password', value: 'password', icon: <Lock size={14} /> },
{ label: t('ssh.remote.privateKey') || 'Private Key', value: 'privateKey', icon: <Key size={14} /> },
{ label: t('ssh.remote.sshAgent') || 'SSH Agent', value: 'agent', icon: <Terminal size={14} /> },
];

const canSubmit = (): boolean => {
const u = username.trim();
if (!u) return false;
if (authMethod === 'password') return password.length > 0;
if (authMethod === 'privateKey') return keyPath.trim().length > 0;
return true;
return keyPath.trim().length > 0;
};

const handleSubmit = () => {
Expand All @@ -88,14 +86,12 @@ export const SSHAuthPromptDialog: React.FC<SSHAuthPromptDialogProps> = ({
let auth: SSHAuthMethod;
if (authMethod === 'password') {
auth = { type: 'Password', password };
} else if (authMethod === 'privateKey') {
} else {
auth = {
type: 'PrivateKey',
keyPath: keyPath.trim(),
passphrase: passphrase.trim() || undefined,
};
} else {
auth = { type: 'Agent' };
}
onSubmit({ auth, username: u });
};
Expand Down Expand Up @@ -154,7 +150,7 @@ export const SSHAuthPromptDialog: React.FC<SSHAuthPromptDialogProps> = ({
<Select
options={authOptions}
value={authMethod}
onChange={(value) => setAuthMethod(value as 'password' | 'privateKey' | 'agent')}
onChange={(value) => setAuthMethod(value as 'password' | 'privateKey')}
size="medium"
disabled={isConnecting}
/>
Expand Down Expand Up @@ -216,10 +212,6 @@ export const SSHAuthPromptDialog: React.FC<SSHAuthPromptDialogProps> = ({
</>
)}

{authMethod === 'agent' && (
<p className="ssh-auth-prompt-dialog__hint">{t('ssh.remote.authPromptAgentHint')}</p>
)}

<div className="ssh-auth-prompt-dialog__actions">
<Button variant="secondary" onClick={onCancel} disabled={isConnecting}>
{t('actions.cancel')}
Expand Down
20 changes: 8 additions & 12 deletions src/web-ui/src/features/ssh-remote/SSHConnectionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Input } from '@/component-library';
import { Select } from '@/component-library';
import { Alert } from '@/component-library';
import { IconButton } from '@/component-library';
import { FolderOpen, Loader2, Server, User, Key, Lock, Terminal, Trash2, Plus, Pencil, Play } from 'lucide-react';
import { FolderOpen, Loader2, Server, User, Key, Lock, Trash2, Plus, Pencil, Play } from 'lucide-react';
import type {
SSHConnectionConfig,
SSHAuthMethod,
Expand Down Expand Up @@ -58,7 +58,7 @@ export const SSHConnectionDialog: React.FC<SSHConnectionDialogProps> = ({
host: '',
port: '22',
username: '',
authType: 'password' as 'password' | 'privateKey' | 'agent',
authType: 'password' as 'password' | 'privateKey',
password: '',
keyPath: '~/.ssh/id_rsa',
passphrase: '',
Expand Down Expand Up @@ -149,8 +149,6 @@ export const SSHConnectionDialog: React.FC<SSHConnectionDialogProps> = ({
keyPath: formData.keyPath,
passphrase: formData.passphrase || undefined,
};
case 'agent':
return { type: 'Agent' };
}
};

Expand Down Expand Up @@ -232,10 +230,11 @@ export const SSHConnectionDialog: React.FC<SSHConnectionDialogProps> = ({
} finally {
setIsConnecting(false);
}
} else {
const auth: SSHAuthMethod = conn.authType.type === 'PrivateKey'
? { type: 'PrivateKey', keyPath: conn.authType.keyPath }
: { type: 'Agent' };
} else if (conn.authType.type === 'PrivateKey') {
const auth: SSHAuthMethod = {
type: 'PrivateKey',
keyPath: conn.authType.keyPath,
};

setIsConnecting(true);
try {
Expand Down Expand Up @@ -368,9 +367,7 @@ export const SSHConnectionDialog: React.FC<SSHConnectionDialogProps> = ({
host: conn.host,
port: String(conn.port),
username: conn.username,
authType: conn.authType.type === 'Password' ? 'password'
: conn.authType.type === 'PrivateKey' ? 'privateKey'
: 'agent',
authType: conn.authType.type === 'Password' ? 'password' : 'privateKey',
password: '',
keyPath,
passphrase: '',
Expand All @@ -390,7 +387,6 @@ export const SSHConnectionDialog: React.FC<SSHConnectionDialogProps> = ({
const authOptions = [
{ label: t('ssh.remote.password') || 'Password', value: 'password', icon: <Lock size={14} /> },
{ label: t('ssh.remote.privateKey') || 'Private Key', value: 'privateKey', icon: <Key size={14} /> },
{ label: t('ssh.remote.sshAgent') || 'SSH Agent', value: 'agent', icon: <Terminal size={14} /> },
];

const dismissError = () => {
Expand Down
2 changes: 0 additions & 2 deletions src/web-ui/src/features/ssh-remote/SSHRemoteProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ export const SSHRemoteProvider: React.FC<SSHRemoteProviderProps> = ({ children }
let authMethod: SSHConnectionConfig['auth'] | null = null;
if (savedConn.authType.type === 'PrivateKey') {
authMethod = { type: 'PrivateKey', keyPath: savedConn.authType.keyPath };
} else if (savedConn.authType.type === 'Agent') {
authMethod = { type: 'Agent' };
} else {
// Caller must only invoke password reconnect when vault has a password (see checkRemoteWorkspace).
authMethod = { type: 'Password', password: '' };
Expand Down
6 changes: 2 additions & 4 deletions src/web-ui/src/features/ssh-remote/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ export interface SSHConnectionConfig {

export type SSHAuthMethod =
| { type: 'Password'; password: string }
| { type: 'PrivateKey'; keyPath: string; passphrase?: string }
| { type: 'Agent' };
| { type: 'PrivateKey'; keyPath: string; passphrase?: string };

export type SavedAuthType =
| { type: 'Password' }
| { type: 'PrivateKey'; keyPath: string }
| { type: 'Agent' };
| { type: 'PrivateKey'; keyPath: string };

export interface SavedConnection {
id: string;
Expand Down
2 changes: 0 additions & 2 deletions src/web-ui/src/locales/en-US/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,6 @@
"authMethod": "Authentication Method",
"password": "Password",
"privateKey": "Private Key",
"sshAgent": "SSH Agent",
"privateKeyPath": "Private Key Path",
"browsePrivateKey": "Choose private key file",
"pickPrivateKeyDialogTitle": "Select SSH private key",
Expand All @@ -963,7 +962,6 @@
"enterKeyPath": "Enter Private Key Path",
"keyPathDescription": "Enter the private key file path",
"authPromptTitle": "SSH authentication",
"authPromptAgentHint": "Uses keys already loaded in your SSH agent.",
"disconnectConfirm": "Are you sure you want to disconnect?",
"disconnectWorkspaceConfirm": "Are you sure you want to close the remote workspace? This will disconnect from the remote server.",
"newFile": "New File",
Expand Down
2 changes: 0 additions & 2 deletions src/web-ui/src/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,6 @@
"authMethod": "认证方式",
"password": "密码",
"privateKey": "私钥",
"sshAgent": "SSH Agent",
"privateKeyPath": "私钥路径",
"browsePrivateKey": "选择私钥文件",
"pickPrivateKeyDialogTitle": "选择 SSH 私钥",
Expand All @@ -963,7 +962,6 @@
"enterKeyPath": "请输入私钥路径",
"keyPathDescription": "输入私钥文件路径",
"authPromptTitle": "SSH 认证",
"authPromptAgentHint": "将使用本机 SSH Agent 中已加载的密钥进行认证。",
"disconnectConfirm": "确定要断开连接吗?",
"disconnectWorkspaceConfirm": "确定要关闭远程工作区吗?这将断开与远程服务器的连接。",
"newFile": "新建文件",
Expand Down
Loading