use std::time::Duration;
use crate::{vault::Vault, Permission, Result};
pub struct NamespacedVault<'a> {
vault: &'a Vault,
namespace: String,
identity: String,
}
impl<'a> NamespacedVault<'a> {
pub(crate) fn new(vault: &'a Vault, namespace: &str, identity: &str) -> Self {
Self {
vault,
namespace: namespace.to_string(),
identity: identity.to_string(),
}
}
fn prefixed_key(&self, key: &str) -> String {
format!("{}:{}", self.namespace, key)
}
fn strip_prefix<'b>(&self, key: &'b str) -> Option<&'b str> {
let prefix = format!("{}:", self.namespace);
key.strip_prefix(&prefix)
}
pub fn set(&self, key: &str, value: &str) -> Result<()> {
let prefixed = self.prefixed_key(key);
self.vault.set(&self.identity, &prefixed, value)
}
pub fn get(&self, key: &str) -> Result<String> {
let prefixed = self.prefixed_key(key);
self.vault.get(&self.identity, &prefixed)
}
pub fn delete(&self, key: &str) -> Result<()> {
let prefixed = self.prefixed_key(key);
self.vault.delete(&self.identity, &prefixed)
}
pub fn rotate(&self, key: &str, new_value: &str) -> Result<()> {
let prefixed = self.prefixed_key(key);
self.vault.rotate(&self.identity, &prefixed, new_value)
}
pub fn list(&self, pattern: &str) -> Result<Vec<String>> {
let ns_pattern = format!("{}:{}", self.namespace, pattern);
let keys = self.vault.list(&self.identity, &ns_pattern)?;
Ok(keys
.into_iter()
.filter_map(|k| self.strip_prefix(&k).map(String::from))
.collect())
}
pub fn grant(&self, entity: &str, key: &str, level: Permission) -> Result<()> {
let prefixed = self.prefixed_key(key);
self.vault
.grant_with_permission(&self.identity, entity, &prefixed, level)
}
pub fn revoke(&self, entity: &str, key: &str) -> Result<()> {
let prefixed = self.prefixed_key(key);
self.vault.revoke(&self.identity, entity, &prefixed)
}
pub fn set_with_ttl(&self, key: &str, value: &str, ttl: Duration) -> Result<()> {
let prefixed = self.prefixed_key(key);
self.vault
.set_with_ttl(&self.identity, &prefixed, value, ttl)
}
pub fn clear_expiration(&self, key: &str) -> Result<()> {
let prefixed = self.prefixed_key(key);
self.vault.clear_expiration(&self.identity, &prefixed)
}
pub fn get_expiration(&self, key: &str) -> Result<Option<i64>> {
let prefixed = self.prefixed_key(key);
self.vault.get_expiration(&self.identity, &prefixed)
}
pub fn encrypt_for(&self, key: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
let prefixed = self.prefixed_key(key);
self.vault.encrypt_for(&self.identity, &prefixed, plaintext)
}
pub fn decrypt_as(&self, key: &str, sealed: &[u8]) -> Result<Vec<u8>> {
let prefixed = self.prefixed_key(key);
self.vault.decrypt_as(&self.identity, &prefixed, sealed)
}
pub fn emergency_access(
&self,
key: &str,
justification: &str,
duration: Duration,
) -> Result<String> {
let prefixed = self.prefixed_key(key);
self.vault
.emergency_access(&self.identity, &prefixed, justification, duration)
}
pub fn batch_get(&self, keys: &[&str]) -> Result<Vec<(String, Result<String>)>> {
let prefixed_keys: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();
let prefixed_refs: Vec<&str> = prefixed_keys.iter().map(String::as_str).collect();
let results = self.vault.batch_get(&self.identity, &prefixed_refs)?;
Ok(results
.into_iter()
.map(|(k, v)| {
let stripped = self
.strip_prefix(&k)
.map_or_else(|| k.clone(), String::from);
(stripped, v)
})
.collect())
}
pub fn batch_set(&self, entries: &[(&str, &str)]) -> Result<()> {
let prefixed: Vec<(String, String)> = entries
.iter()
.map(|(k, v)| (self.prefixed_key(k), (*v).to_string()))
.collect();
let refs: Vec<(&str, &str)> = prefixed
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
self.vault.batch_set(&self.identity, &refs)
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn identity(&self) -> &str {
&self.identity
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use graph_engine::GraphEngine;
use tensor_store::TensorStore;
use crate::vault::Vault;
use crate::{RateLimitConfig, VaultConfig};
fn create_test_vault() -> Vault {
let store = TensorStore::new();
let graph = Arc::new(GraphEngine::new());
Vault::new(b"test_password", graph, store, VaultConfig::default()).unwrap()
}
#[test]
fn test_namespaced_clear_expiration() {
let vault = create_test_vault();
let ns = vault.namespace("prod", Vault::ROOT);
ns.set_with_ttl("ephemeral", "temp_value", Duration::from_secs(3600))
.unwrap();
let exp = ns.get_expiration("ephemeral").unwrap();
assert!(exp.is_some());
ns.clear_expiration("ephemeral").unwrap();
let exp_after = ns.get_expiration("ephemeral").unwrap();
assert!(exp_after.is_none());
assert_eq!(ns.get("ephemeral").unwrap(), "temp_value");
}
#[test]
fn test_namespaced_emergency_access() {
let config = VaultConfig::default().with_rate_limit(RateLimitConfig {
max_gets: 100,
max_lists: 100,
max_sets: 100,
max_grants: 100,
max_break_glass: 10,
max_wraps: 100,
max_generates: 100,
window: Duration::from_secs(60),
});
let store = TensorStore::new();
let graph = Arc::new(GraphEngine::new());
let vault = Vault::new(b"test_password", graph, store, config).unwrap();
let ns_root = vault.namespace("secure", Vault::ROOT);
ns_root.set("classified", "top_secret_data").unwrap();
let ns_bob = vault.namespace("secure", "user:bob");
assert!(ns_bob.get("classified").is_err());
let value = ns_bob
.emergency_access(
"classified",
"Production incident P1-4321",
Duration::from_secs(60),
)
.unwrap();
assert_eq!(value, "top_secret_data");
}
#[test]
fn test_namespaced_batch_set() {
let vault = create_test_vault();
let ns = vault.namespace("env", Vault::ROOT);
let entries = vec![
("DB_HOST", "localhost"),
("DB_PORT", "5432"),
("DB_NAME", "mydb"),
];
ns.batch_set(&entries).unwrap();
assert_eq!(ns.get("DB_HOST").unwrap(), "localhost");
assert_eq!(ns.get("DB_PORT").unwrap(), "5432");
assert_eq!(ns.get("DB_NAME").unwrap(), "mydb");
}
}