- Filter history/rollback/delete by user_id in secrets-core - MCP tools/web pass user context; dashboard refresh; favicon static - .gitignore *.pem; vscode tasks tweaks - clippy: collapse else-if in rollback latest-history branch Made-with: Cursor
123 lines
3.7 KiB
Rust
123 lines
3.7 KiB
Rust
use anyhow::Result;
|
|
use serde_json::Value;
|
|
use sqlx::PgPool;
|
|
use std::collections::HashMap;
|
|
use uuid::Uuid;
|
|
|
|
use crate::crypto;
|
|
use crate::models::Entry;
|
|
use crate::service::search::{fetch_entries, fetch_secrets_for_entries};
|
|
|
|
/// Build an env variable map from entry secrets (for dry-run preview or injection).
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn build_env_map(
|
|
pool: &PgPool,
|
|
namespace: Option<&str>,
|
|
kind: Option<&str>,
|
|
name: Option<&str>,
|
|
tags: &[String],
|
|
only_fields: &[String],
|
|
prefix: &str,
|
|
master_key: &[u8; 32],
|
|
user_id: Option<Uuid>,
|
|
) -> Result<HashMap<String, String>> {
|
|
let entries = fetch_entries(pool, namespace, kind, name, tags, None, user_id).await?;
|
|
|
|
let mut combined: HashMap<String, String> = HashMap::new();
|
|
|
|
for entry in &entries {
|
|
let entry_map = build_entry_env_map(pool, entry, only_fields, prefix, master_key).await?;
|
|
combined.extend(entry_map);
|
|
}
|
|
|
|
Ok(combined)
|
|
}
|
|
|
|
async fn build_entry_env_map(
|
|
pool: &PgPool,
|
|
entry: &Entry,
|
|
only_fields: &[String],
|
|
prefix: &str,
|
|
master_key: &[u8; 32],
|
|
) -> Result<HashMap<String, String>> {
|
|
let entry_ids = vec![entry.id];
|
|
let secrets_map = fetch_secrets_for_entries(pool, &entry_ids).await?;
|
|
let all_fields = secrets_map.get(&entry.id).map(Vec::as_slice).unwrap_or(&[]);
|
|
|
|
let fields: Vec<_> = if only_fields.is_empty() {
|
|
all_fields.iter().collect()
|
|
} else {
|
|
all_fields
|
|
.iter()
|
|
.filter(|f| only_fields.contains(&f.field_name))
|
|
.collect()
|
|
};
|
|
|
|
let effective_prefix = env_prefix(entry, prefix);
|
|
let mut map = HashMap::new();
|
|
|
|
for f in fields {
|
|
let decrypted = crypto::decrypt_json(master_key, &f.encrypted)?;
|
|
let key = format!(
|
|
"{}_{}",
|
|
effective_prefix,
|
|
f.field_name.to_uppercase().replace(['-', '.'], "_")
|
|
);
|
|
map.insert(key, json_to_env_string(&decrypted));
|
|
}
|
|
|
|
// Resolve key_ref
|
|
if let Some(key_ref) = entry.metadata.get("key_ref").and_then(|v| v.as_str()) {
|
|
let key_entries = fetch_entries(
|
|
pool,
|
|
Some(&entry.namespace),
|
|
Some("key"),
|
|
Some(key_ref),
|
|
&[],
|
|
None,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
if let Some(key_entry) = key_entries.first() {
|
|
let key_ids = vec![key_entry.id];
|
|
let key_fields_map = fetch_secrets_for_entries(pool, &key_ids).await?;
|
|
let empty = vec![];
|
|
let key_fields = key_fields_map.get(&key_entry.id).unwrap_or(&empty);
|
|
let key_prefix = env_prefix(key_entry, prefix);
|
|
for f in key_fields {
|
|
let decrypted = crypto::decrypt_json(master_key, &f.encrypted)?;
|
|
let key_var = format!(
|
|
"{}_{}",
|
|
key_prefix,
|
|
f.field_name.to_uppercase().replace(['-', '.'], "_")
|
|
);
|
|
map.insert(key_var, json_to_env_string(&decrypted));
|
|
}
|
|
} else {
|
|
tracing::warn!(key_ref, "key_ref target not found");
|
|
}
|
|
}
|
|
|
|
Ok(map)
|
|
}
|
|
|
|
fn env_prefix(entry: &Entry, prefix: &str) -> String {
|
|
let name_part = entry.name.to_uppercase().replace(['-', '.', ' '], "_");
|
|
if prefix.is_empty() {
|
|
name_part
|
|
} else {
|
|
let normalized = prefix.to_uppercase().replace(['-', '.', ' '], "_");
|
|
let normalized = normalized.trim_end_matches('_');
|
|
format!("{}_{}", normalized, name_part)
|
|
}
|
|
}
|
|
|
|
fn json_to_env_string(v: &Value) -> String {
|
|
match v {
|
|
Value::String(s) => s.clone(),
|
|
Value::Null => String::new(),
|
|
other => other.to_string(),
|
|
}
|
|
}
|