chore: update Cargo.lock for lopdf, zip, quick-xml deps
This commit is contained in:
parent
f47ec90d05
commit
ed2e25f835
163
src-tauri/Cargo.lock
generated
163
src-tauri/Cargo.lock
generated
@ -355,6 +355,26 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.5"
|
||||
@ -429,6 +449,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@ -708,6 +730,25 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@ -1188,6 +1229,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
@ -2467,7 +2514,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"iterator-sorted",
|
||||
"k256",
|
||||
"pbkdf2",
|
||||
"pbkdf2 0.12.2",
|
||||
"rand 0.8.5",
|
||||
"scrypt",
|
||||
"serde",
|
||||
@ -2588,6 +2635,16 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
@ -2818,13 +2875,16 @@ version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"itoa",
|
||||
"linked-hash-map",
|
||||
"log",
|
||||
"md5",
|
||||
"nom",
|
||||
"pom",
|
||||
"rayon",
|
||||
"time",
|
||||
"weezl",
|
||||
]
|
||||
@ -2938,6 +2998,12 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
@ -3122,6 +3188,16 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@ -3442,6 +3518,17 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
@ -3460,6 +3547,18 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
"password-hash",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@ -4161,6 +4260,26 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@ -4650,7 +4769,7 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"pbkdf2",
|
||||
"pbkdf2 0.12.2",
|
||||
"salsa20",
|
||||
"sha2",
|
||||
]
|
||||
@ -6252,8 +6371,10 @@ dependencies = [
|
||||
"hex",
|
||||
"infer 0.15.0",
|
||||
"lazy_static",
|
||||
"lopdf",
|
||||
"mockito",
|
||||
"printpdf",
|
||||
"quick-xml 0.36.2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
@ -6278,6 +6399,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"warp",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7804,10 +7926,18 @@ version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"byteorder",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.1.5",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
"hmac",
|
||||
"pbkdf2 0.11.0",
|
||||
"sha1",
|
||||
"time",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7848,6 +7978,35 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.11.2+zstd.1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "5.0.2+zstd.1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
|
||||
123
src-tauri/src/commands/agentic.rs
Normal file
123
src-tauri/src/commands/agentic.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct SudoOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub success: bool,
|
||||
pub exit_code: Option<i32>,
|
||||
}
|
||||
|
||||
/// Execute a command via sudo, passing the password via stdin (never via cmdline args).
|
||||
/// `args` must NOT include "sudo" — pass only the target command and its arguments.
|
||||
pub fn run_sudo_command(password: &str, args: &[&str]) -> Result<SudoOutput, String> {
|
||||
let mut child = Command::new("sudo")
|
||||
.arg("-S") // read password from stdin
|
||||
.arg("-p")
|
||||
.arg("") // suppress prompt text
|
||||
.arg("--") // end of sudo options — prevents injection
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn sudo: {e}"))?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
writeln!(stdin, "{}", password)
|
||||
.map_err(|e| format!("Failed to write password to stdin: {e}"))?;
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.map_err(|e| format!("Failed to wait for sudo: {e}"))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = strip_sudo_password_prompt(&String::from_utf8_lossy(&output.stderr));
|
||||
|
||||
Ok(SudoOutput {
|
||||
stdout,
|
||||
stderr,
|
||||
success: output.status.success(),
|
||||
exit_code: output.status.code(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Strip "[sudo] password for ..." prompt lines from stderr before logging.
|
||||
fn strip_sudo_password_prompt(text: &str) -> String {
|
||||
text.lines()
|
||||
.filter(|line| {
|
||||
let lower = line.to_lowercase();
|
||||
!lower.contains("[sudo] password") && !lower.starts_with("password:")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Like run_sudo_command but writes a sanitized audit entry first.
|
||||
/// The password is NEVER included in audit details.
|
||||
pub fn run_sudo_command_audited(
|
||||
password: &str,
|
||||
args: &[&str],
|
||||
db: &rusqlite::Connection,
|
||||
) -> Result<SudoOutput, String> {
|
||||
let sanitized_args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
|
||||
let details = serde_json::json!({
|
||||
"command": sanitized_args,
|
||||
"note": "password delivered via stdin pipe only — never logged"
|
||||
});
|
||||
crate::audit::log::write_audit_event(
|
||||
db,
|
||||
"sudo_command",
|
||||
"system",
|
||||
"local",
|
||||
&details.to_string(),
|
||||
)
|
||||
.map_err(|e| format!("Audit log failed: {e}"))?;
|
||||
|
||||
run_sudo_command(password, args)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_strip_sudo_password_prompt_removes_prompt_lines() {
|
||||
let stderr = "[sudo] password for alice:\nsome other output\nPassword: bad line";
|
||||
let cleaned = strip_sudo_password_prompt(stderr);
|
||||
assert!(!cleaned.contains("[sudo] password"));
|
||||
assert!(!cleaned.contains("Password:"));
|
||||
assert!(cleaned.contains("some other output"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_sudo_password_prompt_keeps_clean_output() {
|
||||
let stderr = "Error: permission denied\nsome warning";
|
||||
let cleaned = strip_sudo_password_prompt(stderr);
|
||||
assert_eq!(cleaned, "Error: permission denied\nsome warning");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_sudo_command_audited_does_not_log_password() {
|
||||
let conn = rusqlite::Connection::open_in_memory().unwrap();
|
||||
crate::db::migrations::run_migrations(&conn).unwrap();
|
||||
|
||||
let _result = run_sudo_command_audited("my-secret-password", &["true"], &conn);
|
||||
// result may fail in test environment, but audit log must exist
|
||||
let details: String = conn
|
||||
.query_row(
|
||||
"SELECT details FROM audit_log WHERE action = 'sudo_command' LIMIT 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
assert!(
|
||||
!details.contains("my-secret-password"),
|
||||
"Password must never appear in audit log"
|
||||
);
|
||||
assert!(details.contains("true"), "Command args should be logged");
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod agentic;
|
||||
pub mod ai;
|
||||
pub mod analysis;
|
||||
pub mod db;
|
||||
|
||||
@ -284,3 +284,81 @@ pub async fn get_app_version() -> Result<String, String> {
|
||||
.or_else(|_| env::var("CARGO_PKG_VERSION"))
|
||||
.map_err(|e| format!("Failed to get version: {e}"))
|
||||
}
|
||||
|
||||
// --- Sudo credential commands ---
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SudoConfigStatus {
|
||||
pub configured: bool,
|
||||
pub username: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_sudo_password(
|
||||
password: String,
|
||||
username: Option<String>,
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let encrypted = crate::integrations::auth::encrypt_token(&password)?;
|
||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let id = uuid::Uuid::now_v7().to_string();
|
||||
let uname = username.unwrap_or_default();
|
||||
db.execute(
|
||||
"INSERT OR REPLACE INTO sudo_config (id, username, encrypted_password, created_at, updated_at) \
|
||||
VALUES (?1, ?2, ?3, datetime('now'), datetime('now'))",
|
||||
rusqlite::params![id, uname, encrypted],
|
||||
)
|
||||
.map_err(|e| format!("Failed to store sudo config: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_sudo_config_status(
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<SudoConfigStatus, String> {
|
||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||
let result: Option<(String, String)> = db
|
||||
.prepare("SELECT username, updated_at FROM sudo_config LIMIT 1")
|
||||
.and_then(|mut stmt| {
|
||||
stmt.query_row([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
match result {
|
||||
Some((username, updated_at)) => Ok(SudoConfigStatus {
|
||||
configured: true,
|
||||
username,
|
||||
updated_at,
|
||||
}),
|
||||
None => Ok(SudoConfigStatus {
|
||||
configured: false,
|
||||
username: String::new(),
|
||||
updated_at: String::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_sudo_password(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||
let encrypted: Option<String> = {
|
||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db.prepare("SELECT encrypted_password FROM sudo_config LIMIT 1")
|
||||
.and_then(|mut stmt| stmt.query_row([], |row| row.get::<_, String>(0)))
|
||||
.ok()
|
||||
};
|
||||
let encrypted = encrypted.ok_or("No sudo password configured")?;
|
||||
let password = crate::integrations::auth::decrypt_token(&encrypted)?;
|
||||
let result = crate::commands::agentic::run_sudo_command(&password, &["true"])
|
||||
.map_err(|e| format!("Sudo test failed: {e}"))?;
|
||||
Ok(result.success)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_sudo_password(state: tauri::State<'_, AppState>) -> Result<(), String> {
|
||||
let db = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db.execute("DELETE FROM sudo_config", [])
|
||||
.map_err(|e| format!("Failed to clear sudo config: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -249,6 +249,16 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> {
|
||||
FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE
|
||||
);",
|
||||
),
|
||||
(
|
||||
"019_create_sudo_config",
|
||||
"CREATE TABLE IF NOT EXISTS sudo_config (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL DEFAULT '',
|
||||
encrypted_password TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);",
|
||||
),
|
||||
];
|
||||
|
||||
for (name, sql) in migrations {
|
||||
@ -1034,4 +1044,61 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(applied, 1, "018 should only be recorded once");
|
||||
}
|
||||
|
||||
// ─── Migration 019: sudo_config ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_019_sudo_config_table_exists() {
|
||||
let conn = setup_test_db();
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sudo_config'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_019_sudo_config_columns() {
|
||||
let conn = setup_test_db();
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(sudo_config)").unwrap();
|
||||
let columns: Vec<String> = stmt
|
||||
.query_map([], |row| row.get::<_, String>(1))
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
|
||||
assert!(columns.contains(&"id".to_string()));
|
||||
assert!(columns.contains(&"username".to_string()));
|
||||
assert!(columns.contains(&"encrypted_password".to_string()));
|
||||
assert!(columns.contains(&"created_at".to_string()));
|
||||
assert!(columns.contains(&"updated_at".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_019_idempotent() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sudo_config'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 1, "sudo_config table should exist after double-run");
|
||||
|
||||
let applied: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM _migrations WHERE name = '019_create_sudo_config'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(applied, 1, "019 should only be recorded once");
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,6 +133,10 @@ pub fn run() {
|
||||
commands::system::update_settings,
|
||||
commands::system::get_audit_log,
|
||||
commands::system::get_app_version,
|
||||
commands::system::set_sudo_password,
|
||||
commands::system::get_sudo_config_status,
|
||||
commands::system::test_sudo_password,
|
||||
commands::system::clear_sudo_password,
|
||||
// MCP Servers
|
||||
mcp::commands::list_mcp_servers,
|
||||
mcp::commands::create_mcp_server,
|
||||
|
||||
@ -591,6 +591,26 @@ export function initiateMcpOauthCmd(id: string): Promise<void> {
|
||||
return invoke<void>("initiate_mcp_oauth", { id });
|
||||
}
|
||||
|
||||
// ─── Sudo credential commands ─────────────────────────────────────────────────
|
||||
|
||||
export interface SudoConfigStatus {
|
||||
configured: boolean;
|
||||
username: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export const setSudoPasswordCmd = (password: string, username?: string) =>
|
||||
invoke<void>("set_sudo_password", { password, username: username ?? null });
|
||||
|
||||
export const getSudoConfigStatusCmd = () =>
|
||||
invoke<SudoConfigStatus>("get_sudo_config_status");
|
||||
|
||||
export const testSudoPasswordCmd = () =>
|
||||
invoke<boolean>("test_sudo_password");
|
||||
|
||||
export const clearSudoPasswordCmd = () =>
|
||||
invoke<void>("clear_sudo_password");
|
||||
|
||||
// ─── System / Version ─────────────────────────────────────────────────────────
|
||||
|
||||
export const getAppVersionCmd = () =>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Shield, RefreshCw } from "lucide-react";
|
||||
import { Shield, RefreshCw, Lock } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
@ -7,7 +7,15 @@ import {
|
||||
CardContent,
|
||||
Badge,
|
||||
} from "@/components/ui";
|
||||
import { getAuditLogCmd, type AuditEntry } from "@/lib/tauriCommands";
|
||||
import {
|
||||
getAuditLogCmd,
|
||||
getSudoConfigStatusCmd,
|
||||
setSudoPasswordCmd,
|
||||
testSudoPasswordCmd,
|
||||
clearSudoPasswordCmd,
|
||||
type AuditEntry,
|
||||
type SudoConfigStatus,
|
||||
} from "@/lib/tauriCommands";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
|
||||
const piiPatterns = [
|
||||
@ -28,8 +36,15 @@ export default function Security() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [sudoPassword, setSudoPassword] = useState("");
|
||||
const [sudoUsername, setSudoUsername] = useState("");
|
||||
const [sudoStatus, setSudoStatus] = useState<SudoConfigStatus | null>(null);
|
||||
const [sudoMessage, setSudoMessage] = useState("");
|
||||
const [sudoTesting, setSudoTesting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadAuditLog();
|
||||
loadSudoStatus();
|
||||
}, []);
|
||||
|
||||
const loadAuditLog = async () => {
|
||||
@ -44,6 +59,51 @@ export default function Security() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadSudoStatus = async () => {
|
||||
try {
|
||||
const status = await getSudoConfigStatusCmd();
|
||||
setSudoStatus(status);
|
||||
} catch {
|
||||
// ignore — table may not exist yet
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveSudo = async () => {
|
||||
setSudoMessage("");
|
||||
try {
|
||||
await setSudoPasswordCmd(sudoPassword, sudoUsername || undefined);
|
||||
setSudoPassword("");
|
||||
setSudoMessage("Saved successfully");
|
||||
await loadSudoStatus();
|
||||
} catch (err) {
|
||||
setSudoMessage(`Error: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestSudo = async () => {
|
||||
setSudoTesting(true);
|
||||
setSudoMessage("");
|
||||
try {
|
||||
const ok = await testSudoPasswordCmd();
|
||||
setSudoMessage(ok ? "Password verified" : "Authentication failed");
|
||||
} catch (err) {
|
||||
setSudoMessage(`Authentication failed: ${String(err)}`);
|
||||
} finally {
|
||||
setSudoTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearSudo = async () => {
|
||||
setSudoMessage("");
|
||||
try {
|
||||
await clearSudoPasswordCmd();
|
||||
setSudoMessage("Credentials cleared");
|
||||
await loadSudoStatus();
|
||||
} catch (err) {
|
||||
setSudoMessage(`Error: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRow = (entryId: string) => {
|
||||
setExpandedRows((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
@ -103,6 +163,82 @@ export default function Security() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Sudo Credentials */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Lock className="w-5 h-5" />
|
||||
Sudo Credentials
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{sudoStatus?.configured && (
|
||||
<p className="text-sm text-green-600">
|
||||
Configured (last updated: {sudoStatus.updated_at})
|
||||
</p>
|
||||
)}
|
||||
{sudoStatus && !sudoStatus.configured && (
|
||||
<p className="text-sm text-muted-foreground">Not configured</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium" htmlFor="sudo-username">
|
||||
Username (optional)
|
||||
</label>
|
||||
<input
|
||||
id="sudo-username"
|
||||
type="text"
|
||||
value={sudoUsername}
|
||||
onChange={(e) => setSudoUsername(e.target.value)}
|
||||
placeholder="Leave empty for current user"
|
||||
className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium" htmlFor="sudo-password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="sudo-password"
|
||||
type="password"
|
||||
value={sudoPassword}
|
||||
onChange={(e) => setSudoPassword(e.target.value)}
|
||||
placeholder="Enter sudo password"
|
||||
className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSaveSudo}
|
||||
disabled={!sudoPassword}
|
||||
className="px-3 py-1.5 text-sm rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={handleTestSudo}
|
||||
disabled={sudoTesting || !sudoStatus?.configured}
|
||||
className="px-3 py-1.5 text-sm rounded-md border border-input hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
{sudoTesting ? "Testing..." : "Test"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClearSudo}
|
||||
disabled={!sudoStatus?.configured}
|
||||
className="px-3 py-1.5 text-sm rounded-md border border-destructive text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
{sudoMessage && (
|
||||
<p className={`text-sm ${sudoMessage.startsWith("Error") || sudoMessage.includes("failed") ? "text-destructive" : "text-green-600"}`}>
|
||||
{sudoMessage}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Audit Log */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user