feat: use file-based secret store in debug mode to avoid macOS Keychain prompts
During development, every Rust recompile changes the binary signature, causing macOS to invalidate Keychain "Always Allow" and prompt for the password on every hot reload. Switch to a JSON file store in debug builds so dev workflow is uninterrupted; release builds still use the system keychain.
This commit is contained in:
parent
4efd7c2517
commit
40a67434ee
|
|
@ -4,7 +4,7 @@ use tauri::{AppHandle, Manager, State};
|
|||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::commands::connection_secrets::{
|
||||
load_connections_from_file, save_connections_to_file, KeyringConnectionSecretStore,
|
||||
create_secret_store, load_connections_from_file, save_connections_to_file,
|
||||
};
|
||||
use crate::commands::query_cancel::RunningQueries;
|
||||
use crate::db;
|
||||
|
|
@ -233,15 +233,15 @@ pub async fn save_connections(
|
|||
configs: Vec<ConnectionConfig>,
|
||||
) -> Result<(), String> {
|
||||
let path = connections_file(&app)?;
|
||||
let store = KeyringConnectionSecretStore;
|
||||
save_connections_to_file(&path, &configs, &store)
|
||||
let store = create_secret_store(&app);
|
||||
save_connections_to_file(&path, &configs, &*store)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_connections(app: AppHandle) -> Result<Vec<ConnectionConfig>, String> {
|
||||
let path = connections_file(&app)?;
|
||||
let store = KeyringConnectionSecretStore;
|
||||
load_connections_from_file(&path, &store)
|
||||
let store = create_secret_store(&app);
|
||||
load_connections_from_file(&path, &*store)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use crate::models::connection::ConnectionConfig;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub(super) const MAIN_PASSWORD_KEY: &str = "password";
|
||||
pub(super) const SSH_PASSWORD_KEY: &str = "ssh_password";
|
||||
|
|
@ -42,10 +44,60 @@ impl ConnectionSecretStore for KeyringConnectionSecretStore {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) struct FileSecretStore {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileSecretStore {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
fn read_store(&self) -> HashMap<String, String> {
|
||||
std::fs::read_to_string(&self.path)
|
||||
.ok()
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn write_store(&self, map: &HashMap<String, String>) -> Result<(), String> {
|
||||
let json = serde_json::to_string_pretty(map).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&self.path, json).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionSecretStore for FileSecretStore {
|
||||
fn set_secret(&self, connection_id: &str, key: &str, secret: &str) -> Result<(), String> {
|
||||
let mut map = self.read_store();
|
||||
map.insert(secret_account(connection_id, key), secret.to_string());
|
||||
self.write_store(&map)
|
||||
}
|
||||
|
||||
fn get_secret(&self, connection_id: &str, key: &str) -> Result<Option<String>, String> {
|
||||
Ok(self.read_store().get(&secret_account(connection_id, key)).cloned())
|
||||
}
|
||||
|
||||
fn delete_secret(&self, connection_id: &str, key: &str) -> Result<(), String> {
|
||||
let mut map = self.read_store();
|
||||
map.remove(&secret_account(connection_id, key));
|
||||
self.write_store(&map)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn create_secret_store(app: &AppHandle) -> Box<dyn ConnectionSecretStore> {
|
||||
if cfg!(debug_assertions) {
|
||||
let dir = app.path().app_data_dir().expect("failed to resolve app data dir");
|
||||
std::fs::create_dir_all(&dir).ok();
|
||||
Box::new(FileSecretStore::new(dir.join("secrets.json")))
|
||||
} else {
|
||||
Box::new(KeyringConnectionSecretStore)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn save_connections_to_file(
|
||||
path: &Path,
|
||||
configs: &[ConnectionConfig],
|
||||
store: &impl ConnectionSecretStore,
|
||||
store: &dyn ConnectionSecretStore,
|
||||
) -> Result<(), String> {
|
||||
delete_removed_connection_secrets(path, configs, store)?;
|
||||
for config in configs {
|
||||
|
|
@ -64,7 +116,7 @@ pub(super) fn save_connections_to_file(
|
|||
|
||||
pub(super) fn load_connections_from_file(
|
||||
path: &Path,
|
||||
store: &impl ConnectionSecretStore,
|
||||
store: &dyn ConnectionSecretStore,
|
||||
) -> Result<Vec<ConnectionConfig>, String> {
|
||||
if !path.exists() {
|
||||
return Ok(vec![]);
|
||||
|
|
@ -118,7 +170,7 @@ pub(super) fn load_connections_from_file(
|
|||
fn delete_removed_connection_secrets(
|
||||
path: &Path,
|
||||
configs: &[ConnectionConfig],
|
||||
store: &impl ConnectionSecretStore,
|
||||
store: &dyn ConnectionSecretStore,
|
||||
) -> Result<(), String> {
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
|
|
@ -141,7 +193,7 @@ fn delete_removed_connection_secrets(
|
|||
}
|
||||
|
||||
fn persist_secret(
|
||||
store: &impl ConnectionSecretStore,
|
||||
store: &dyn ConnectionSecretStore,
|
||||
connection_id: &str,
|
||||
key: &str,
|
||||
secret: &str,
|
||||
|
|
@ -154,7 +206,7 @@ fn persist_secret(
|
|||
}
|
||||
|
||||
fn persist_optional_secret(
|
||||
store: &impl ConnectionSecretStore,
|
||||
store: &dyn ConnectionSecretStore,
|
||||
connection_id: &str,
|
||||
key: &str,
|
||||
secret: Option<&str>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue