feat(usable): integrate current dbtool implementation snapshot
Some checks failed
release-smoke / macos-13 / x86_64-apple-darwin (push) Has been cancelled
release-smoke / ubuntu-latest / x86_64-unknown-linux-gnu (push) Has been cancelled
release-smoke / windows-latest / x86_64-pc-windows-msvc (push) Has been cancelled

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Paperclip CTO
2026-04-02 08:26:18 +00:00
parent a28dab4cd9
commit d5f69462b0
73 changed files with 5895 additions and 322 deletions

View File

@@ -1,6 +1,13 @@
use db_core::{ConnectionTarget, CoreError};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{env, error::Error, fmt, fs, io};
#[derive(Clone, Debug, PartialEq, Eq)]
pub const PROFILE_STORE_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConnectionProfile {
pub name: String,
pub target: ConnectionTarget,
@@ -46,11 +53,350 @@ impl ConnectionProfile {
self.target.password = password;
self
}
pub fn without_password(mut self) -> Self {
self.target.password = None;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConnectionProfileCatalog {
#[serde(default = "profile_store_version")]
pub version: u32,
#[serde(default)]
pub active_profile: Option<String>,
#[serde(default)]
pub profiles: Vec<ConnectionProfile>,
}
impl Default for ConnectionProfileCatalog {
fn default() -> Self {
Self::empty()
}
}
impl ConnectionProfileCatalog {
pub fn empty() -> Self {
Self {
version: PROFILE_STORE_VERSION,
active_profile: None,
profiles: Vec::new(),
}
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.version != PROFILE_STORE_VERSION {
return Err(ConfigError::UnsupportedVersion(self.version));
}
let mut seen_names = BTreeSet::new();
for profile in &self.profiles {
profile.validate().map_err(ConfigError::Core)?;
if profile.target.password.is_some() {
return Err(ConfigError::SecretPersistenceForbidden(
profile.name.clone(),
));
}
if !seen_names.insert(profile.name.clone()) {
return Err(ConfigError::DuplicateProfileName(profile.name.clone()));
}
}
if let Some(active_profile) = &self.active_profile {
if self.profile(active_profile).is_none() {
return Err(ConfigError::MissingActiveProfile(active_profile.clone()));
}
}
Ok(())
}
pub fn profile(&self, name: &str) -> Option<&ConnectionProfile> {
self.profiles.iter().find(|profile| profile.name == name)
}
pub fn active_profile(&self) -> Option<&ConnectionProfile> {
self.active_profile
.as_deref()
.and_then(|name| self.profile(name))
}
pub fn upsert_profile(&mut self, profile: ConnectionProfile) -> Result<(), ConfigError> {
let sanitized = profile.without_password();
sanitized.validate().map_err(ConfigError::Core)?;
if let Some(existing) = self
.profiles
.iter_mut()
.find(|existing| existing.name == sanitized.name)
{
*existing = sanitized;
} else {
self.profiles.push(sanitized);
}
self.validate()
}
pub fn remove_profile(&mut self, name: &str) -> Option<ConnectionProfile> {
let index = self
.profiles
.iter()
.position(|profile| profile.name == name)?;
let removed = self.profiles.remove(index);
if self.active_profile.as_deref() == Some(name) {
self.active_profile = None;
}
Some(removed)
}
pub fn set_active_profile(&mut self, name: Option<&str>) -> Result<(), ConfigError> {
match name {
Some(name) => {
if self.profile(name).is_none() {
return Err(ConfigError::ProfileNotFound(name.to_string()));
}
self.active_profile = Some(name.to_string());
}
None => {
self.active_profile = None;
}
}
self.validate()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SessionSecretStore {
secrets: BTreeMap<String, String>,
}
impl SessionSecretStore {
pub fn set(
&mut self,
profile_name: impl Into<String>,
secret: impl Into<String>,
) -> Result<(), ConfigError> {
let profile_name = profile_name.into();
let secret = secret.into();
if profile_name.trim().is_empty() {
return Err(ConfigError::InvalidSessionSecret(
"profile name is empty".into(),
));
}
if secret.is_empty() {
return Err(ConfigError::InvalidSessionSecret(
"session secret is empty".into(),
));
}
self.secrets.insert(profile_name, secret);
Ok(())
}
pub fn remove(&mut self, profile_name: &str) -> Option<String> {
self.secrets.remove(profile_name)
}
pub fn contains(&self, profile_name: &str) -> bool {
self.secrets.contains_key(profile_name)
}
pub fn resolve_password(&self, profile: &ConnectionProfile) -> Option<String> {
self.secrets.get(&profile.name).cloned().or_else(|| {
profile
.password_env_var
.as_ref()
.and_then(|env_name| env::var(env_name).ok())
})
}
pub fn materialize_profile(&self, profile: &ConnectionProfile) -> ConnectionProfile {
profile
.clone()
.with_password(self.resolve_password(profile))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConnectionProfileStore {
path: PathBuf,
}
impl ConnectionProfileStore {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn load(&self) -> Result<ConnectionProfileCatalog, ConfigError> {
if !self.path.exists() {
return Ok(ConnectionProfileCatalog::empty());
}
let body = fs::read_to_string(&self.path).map_err(|source| ConfigError::Io {
path: self.path.clone(),
operation: "read",
source,
})?;
let catalog =
serde_json::from_str::<ConnectionProfileCatalog>(&body).map_err(|source| {
ConfigError::Parse {
path: self.path.clone(),
source,
}
})?;
catalog.validate()?;
Ok(catalog)
}
pub fn save(&self, catalog: &ConnectionProfileCatalog) -> Result<(), ConfigError> {
catalog.validate()?;
if let Some(parent) = self.path.parent() {
fs::create_dir_all(parent).map_err(|source| ConfigError::Io {
path: parent.to_path_buf(),
operation: "create_dir_all",
source,
})?;
}
let body = serde_json::to_string_pretty(catalog).map_err(ConfigError::Serialize)?;
let temp_path = self.temporary_path();
fs::write(&temp_path, body).map_err(|source| ConfigError::Io {
path: temp_path.clone(),
operation: "write",
source,
})?;
if self.path.exists() {
fs::remove_file(&self.path).map_err(|source| ConfigError::Io {
path: self.path.clone(),
operation: "remove",
source,
})?;
}
fs::rename(&temp_path, &self.path).map_err(|source| ConfigError::Io {
path: self.path.clone(),
operation: "rename",
source,
})?;
Ok(())
}
fn temporary_path(&self) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
self.path.with_extension(format!("tmp-{unique}"))
}
}
fn profile_store_version() -> u32 {
PROFILE_STORE_VERSION
}
#[derive(Debug)]
pub enum ConfigError {
Core(CoreError),
DuplicateProfileName(String),
MissingActiveProfile(String),
ProfileNotFound(String),
SecretPersistenceForbidden(String),
InvalidSessionSecret(String),
UnsupportedVersion(u32),
Io {
path: PathBuf,
operation: &'static str,
source: io::Error,
},
Parse {
path: PathBuf,
source: serde_json::Error,
},
Serialize(serde_json::Error),
}
impl fmt::Display for ConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Core(error) => error.fmt(formatter),
Self::DuplicateProfileName(name) => {
write!(formatter, "connection profile `{name}` is duplicated")
}
Self::MissingActiveProfile(name) => {
write!(
formatter,
"active profile `{name}` is missing from the catalog"
)
}
Self::ProfileNotFound(name) => {
write!(formatter, "connection profile `{name}` was not found")
}
Self::SecretPersistenceForbidden(name) => write!(
formatter,
"connection profile `{name}` contains an in-memory secret and cannot be persisted"
),
Self::InvalidSessionSecret(message) => formatter.write_str(message),
Self::UnsupportedVersion(version) => write!(
formatter,
"connection profile store version `{version}` is unsupported"
),
Self::Io {
path,
operation,
source,
} => write!(
formatter,
"failed to {operation} connection profile store {}: {source}",
path.display()
),
Self::Parse { path, source } => write!(
formatter,
"failed to parse connection profile store {}: {source}",
path.display()
),
Self::Serialize(source) => write!(
formatter,
"failed to serialize connection profile store: {source}"
),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Core(error) => Some(error),
Self::Io { source, .. } => Some(source),
Self::Parse { source, .. } => Some(source),
Self::Serialize(source) => Some(source),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use db_core::{ConnectionTarget, ConnectionTransport, DatabaseKind};
use db_core::{ConnectionTransport, DatabaseKind};
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
@@ -120,4 +466,108 @@ mod tests {
"postgres://dbtool@127.0.0.1:55432/dbtool_demo via password env DBTOOL_PASSWORD"
);
}
#[test]
fn catalog_rejects_profiles_with_in_memory_passwords() {
let profile = sample_postgres_profile().with_password(Some(String::from("secret")));
let catalog = ConnectionProfileCatalog {
version: PROFILE_STORE_VERSION,
active_profile: Some(String::from("local")),
profiles: vec![profile],
};
let error = catalog
.validate()
.expect_err("catalog should reject secrets");
assert!(matches!(
error,
ConfigError::SecretPersistenceForbidden(name) if name == "local"
));
}
#[test]
fn store_roundtrip_preserves_active_profile_without_password() {
let path = temp_store_path("db-config-store-roundtrip");
let store = ConnectionProfileStore::new(&path);
let mut catalog = ConnectionProfileCatalog::empty();
catalog
.upsert_profile(sample_postgres_profile())
.expect("profile should be inserted");
catalog
.set_active_profile(Some("local"))
.expect("active profile should be valid");
store.save(&catalog).expect("catalog should save");
let raw = fs::read_to_string(&path).expect("store file should exist");
let restored = store.load().expect("catalog should load");
assert!(raw.contains("\"active_profile\": \"local\""));
assert!(!raw.contains("\"password\":"));
assert_eq!(restored.active_profile.as_deref(), Some("local"));
assert_eq!(restored.profiles.len(), 1);
assert_eq!(restored.profiles[0].target.password, None);
fs::remove_file(path).expect("temporary store should be removed");
}
#[test]
fn session_secret_materializes_runtime_profile() {
let profile = sample_postgres_profile();
let mut secrets = SessionSecretStore::default();
secrets
.set("local", "from-session")
.expect("session secret should be set");
let materialized = secrets.materialize_profile(&profile);
assert_eq!(
materialized.target.password.as_deref(),
Some("from-session")
);
}
#[test]
fn removing_active_profile_clears_active_pointer() {
let mut catalog = ConnectionProfileCatalog::empty();
catalog
.upsert_profile(sample_postgres_profile())
.expect("profile should be inserted");
catalog
.set_active_profile(Some("local"))
.expect("active profile should be set");
let removed = catalog.remove_profile("local");
assert!(removed.is_some());
assert_eq!(catalog.active_profile, None);
}
fn sample_postgres_profile() -> ConnectionProfile {
ConnectionProfile::new(
"local",
ConnectionTarget::new(
DatabaseKind::Postgres,
ConnectionTransport::Tcp {
host: String::from("127.0.0.1"),
port: 55432,
},
Some(String::from("dbtool_demo")),
Some(String::from("dbtool")),
)
.expect("target should be valid"),
Some(String::from("DBTOOL_PASSWORD")),
)
.expect("profile should be valid")
}
fn temp_store_path(prefix: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
env::temp_dir().join(format!("{prefix}-{unique}.json"))
}
}