fixed DEL showing wrong deletion amount + AGE LIST now returns a list of managed keys names without nested arrays or labels

This commit is contained in:
Maxime Van Hees
2025-09-18 00:19:40 +02:00
parent b8ca73397d
commit c6b277cc9c
4 changed files with 40 additions and 173 deletions

View File

@@ -19,6 +19,7 @@ use age::x25519;
use ed25519_dalek::{Signature, Signer, Verifier, SigningKey, VerifyingKey}; use ed25519_dalek::{Signature, Signer, Verifier, SigningKey, VerifyingKey};
use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use std::collections::HashSet;
use crate::protocol::Protocol; use crate::protocol::Protocol;
use crate::server::Server; use crate::server::Server;
@@ -276,33 +277,31 @@ pub async fn cmd_age_verify_name(server: &Server, name: &str, message: &str, sig
} }
pub async fn cmd_age_list(server: &Server) -> Protocol { pub async fn cmd_age_list(server: &Server) -> Protocol {
// Returns 4 arrays: ["encpub", <names...>], ["encpriv", ...], ["signpub", ...], ["signpriv", ...] // Return a flat, deduplicated, sorted list of managed key names (no labels)
let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) }; let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) };
let pull = |pat: &str, prefix: &str| -> Result<Vec<String>, DBError> { let pull = |pat: &str, prefix: &str| -> Result<Vec<String>, DBError> {
let keys = st.keys(pat)?; let keys = st.keys(pat)?;
let mut names: Vec<String> = keys.into_iter() let mut names: Vec<String> = keys
.into_iter()
.filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string())) .filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string()))
.collect(); .collect();
names.sort(); names.sort();
Ok(names) Ok(names)
}; };
let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
let signpriv= match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) }; let signpriv = match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
let to_arr = |label: &str, v: Vec<String>| { let mut set: HashSet<String> = HashSet::new();
let mut out = vec![Protocol::BulkString(label.to_string())]; for n in encpub.into_iter().chain(encpriv).chain(signpub).chain(signpriv) {
out.push(Protocol::Array(v.into_iter().map(Protocol::BulkString).collect())); set.insert(n);
Protocol::Array(out) }
};
Protocol::Array(vec![ let mut names: Vec<String> = set.into_iter().collect();
to_arr("encpub", encpub), names.sort();
to_arr("encpriv", encpriv),
to_arr("signpub", signpub), Protocol::Array(names.into_iter().map(Protocol::BulkString).collect())
to_arr("signpriv", signpriv),
])
} }

View File

@@ -1210,8 +1210,13 @@ async fn del_cmd(server: &Server, k: &str) -> Result<Protocol, DBError> {
if !server.has_write_permission() { if !server.has_write_permission() {
return Ok(Protocol::err("ERR write permission denied")); return Ok(Protocol::err("ERR write permission denied"));
} }
server.current_storage()?.del(k.to_string())?; let storage = server.current_storage()?;
Ok(Protocol::SimpleString("1".to_string())) if storage.exists(k)? {
storage.del(k.to_string())?;
Ok(Protocol::SimpleString("1".to_string()))
} else {
Ok(Protocol::SimpleString("0".to_string()))
}
} }
async fn set_ex_cmd( async fn set_ex_cmd(
@@ -1322,6 +1327,9 @@ async fn mset_cmd(server: &Server, pairs: &[(String, String)]) -> Result<Protoco
// DEL with multiple keys: return count of keys actually deleted // DEL with multiple keys: return count of keys actually deleted
async fn del_multi_cmd(server: &Server, keys: &[String]) -> Result<Protocol, DBError> { async fn del_multi_cmd(server: &Server, keys: &[String]) -> Result<Protocol, DBError> {
if !server.has_write_permission() {
return Ok(Protocol::err("ERR write permission denied"));
}
let storage = server.current_storage()?; let storage = server.current_storage()?;
let mut deleted = 0i64; let mut deleted = 0i64;
for k in keys { for k in keys {

View File

@@ -48,20 +48,7 @@ pub enum Permissions {
ReadWrite, ReadWrite,
} }
/// Access key information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessKey {
pub hash: String,
pub permissions: Permissions,
pub created_at: u64,
}
/// Database metadata containing access keys
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseMeta {
pub public: bool,
pub keys: HashMap<String, AccessKey>,
}
/// Access key information returned by RPC /// Access key information returned by RPC
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -133,14 +120,8 @@ pub struct RpcServerImpl {
base_dir: String, base_dir: String,
/// Managed database servers /// Managed database servers
servers: Arc<RwLock<HashMap<u64, Arc<Server>>>>, servers: Arc<RwLock<HashMap<u64, Arc<Server>>>>,
/// Next unencrypted database ID to assign
next_unencrypted_id: Arc<RwLock<u64>>,
/// Next encrypted database ID to assign
next_encrypted_id: Arc<RwLock<u64>>,
/// Default backend type /// Default backend type
backend: crate::options::BackendType, backend: crate::options::BackendType,
/// Encryption keys for databases
encryption_keys: Arc<RwLock<HashMap<u64, Option<String>>>>,
/// Admin secret used to encrypt DB 0 and authorize admin access /// Admin secret used to encrypt DB 0 and authorize admin access
admin_secret: String, admin_secret: String,
} }
@@ -151,10 +132,7 @@ impl RpcServerImpl {
Self { Self {
base_dir, base_dir,
servers: Arc::new(RwLock::new(HashMap::new())), servers: Arc::new(RwLock::new(HashMap::new())),
next_unencrypted_id: Arc::new(RwLock::new(0)),
next_encrypted_id: Arc::new(RwLock::new(10)),
backend, backend,
encryption_keys: Arc::new(RwLock::new(HashMap::new())),
admin_secret, admin_secret,
} }
} }
@@ -212,124 +190,10 @@ impl RpcServerImpl {
.unwrap_or_default() .unwrap_or_default()
} }
/// Get the next available database ID
async fn get_next_db_id(&self, is_encrypted: bool) -> u64 {
if is_encrypted {
let mut id = self.next_encrypted_id.write().await;
let current_id = *id;
*id += 1;
current_id
} else {
let mut id = self.next_unencrypted_id.write().await;
let current_id = *id;
*id += 1;
current_id
}
}
/// Load database metadata from file (static version)
pub async fn load_meta_static(base_dir: &str, db_id: u64) -> Result<DatabaseMeta, jsonrpsee::types::ErrorObjectOwned> {
let meta_path = std::path::PathBuf::from(base_dir).join(format!("{}_meta.json", db_id));
// If meta file doesn't exist, create and persist default
if !meta_path.exists() {
let default_meta = DatabaseMeta {
public: true,
keys: HashMap::new(),
};
// Persist default metadata to disk
Self::save_meta_static(base_dir, db_id, &default_meta).await?;
return Ok(default_meta);
}
// Read file as UTF-8 JSON
let json_str = std::fs::read_to_string(&meta_path)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to read meta file: {}", e),
None::<()>
))?;
serde_json::from_str(&json_str)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to parse meta JSON: {}", e),
None::<()>
))
}
/// Load database metadata from file
async fn load_meta(&self, db_id: u64) -> Result<DatabaseMeta, jsonrpsee::types::ErrorObjectOwned> {
let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id));
// If meta file doesn't exist, create and persist default
if !meta_path.exists() {
let default_meta = DatabaseMeta {
public: true,
keys: HashMap::new(),
};
self.save_meta(db_id, &default_meta).await?;
return Ok(default_meta);
}
// Read file as UTF-8 JSON (meta files are always plain JSON)
let json_str = std::fs::read_to_string(&meta_path)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to read meta file: {}", e),
None::<()>
))?;
serde_json::from_str(&json_str)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to parse meta JSON: {}", e),
None::<()>
))
}
/// Save database metadata to file (static version)
pub async fn save_meta_static(base_dir: &str, db_id: u64, meta: &DatabaseMeta) -> Result<(), jsonrpsee::types::ErrorObjectOwned> {
let meta_path = std::path::PathBuf::from(base_dir).join(format!("{}_meta.json", db_id));
let json_str = serde_json::to_string_pretty(meta)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to serialize meta: {}", e),
None::<()>
))?;
std::fs::write(&meta_path, json_str)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to write meta file: {}", e),
None::<()>
))?;
Ok(())
}
/// Save database metadata to file
async fn save_meta(&self, db_id: u64, meta: &DatabaseMeta) -> Result<(), jsonrpsee::types::ErrorObjectOwned> {
let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id));
let json_str = serde_json::to_string_pretty(meta)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to serialize meta: {}", e),
None::<()>
))?;
// Meta files are always stored as plain JSON (even when data DB is encrypted)
std::fs::write(&meta_path, json_str)
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
-32000,
format!("Failed to write meta file: {}", e),
None::<()>
))?;
Ok(())
}
/// Build database file path for given server/db_id /// Build database file path for given server/db_id
fn db_file_path(&self, server: &Server, db_id: u64) -> std::path::PathBuf { fn db_file_path(&self, server: &Server, db_id: u64) -> std::path::PathBuf {
@@ -374,29 +238,21 @@ impl RpcServerImpl {
(created, last_access) (created, last_access)
} }
/// Compose a DatabaseInfo by probing storage, metadata and filesystem /// Compose a DatabaseInfo by probing storage and filesystem, with admin meta for access key count
async fn build_database_info(&self, db_id: u64, server: &Server) -> DatabaseInfo { async fn build_database_info(&self, db_id: u64, server: &Server) -> DatabaseInfo {
// Probe storage to determine encryption state // Probe storage to determine encryption state
let storage = server.current_storage().ok(); let storage = server.current_storage().ok();
let encrypted = storage.as_ref().map(|s| s.is_encrypted()).unwrap_or(server.option.encrypt); let encrypted = storage.as_ref().map(|s| s.is_encrypted()).unwrap_or(server.option.encrypt);
// Load meta to get access key count // Access key count via admin DB 0
let meta = Self::load_meta_static(&self.base_dir, db_id).await.unwrap_or(DatabaseMeta { let key_count = admin_meta::list_access_keys(&self.base_dir, self.backend.clone(), &self.admin_secret, db_id)
public: true, .map(|v| v.len() as u64)
keys: HashMap::new(), .ok();
});
let key_count = Some(meta.keys.len() as u64);
// Compute size on disk and timestamps // Compute size on disk and timestamps from the DB file path
let db_path = self.db_file_path(server, db_id); let db_path = self.db_file_path(server, db_id);
let size_on_disk = self.compute_size_on_disk(&db_path); let size_on_disk = self.compute_size_on_disk(&db_path);
let (created_at, last_access) = Self::get_file_times_secs(&db_path);
let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id));
let (created_at, last_access) = if meta_path.exists() {
Self::get_file_times_secs(&meta_path)
} else {
Self::get_file_times_secs(&db_path)
};
let backend = match server.option.backend { let backend = match server.option.backend {
crate::options::BackendType::Redb => BackendType::Redb, crate::options::BackendType::Redb => BackendType::Redb,
@@ -468,7 +324,7 @@ impl RpcServer for RpcServerImpl {
Ok(db_id) Ok(db_id)
} }
async fn set_encryption(&self, db_id: u64, _encryption_key: String) -> RpcResult<bool> { async fn set_encryption(&self, _db_id: u64, _encryption_key: String) -> RpcResult<bool> {
// For now, return false as encryption can only be set during creation // For now, return false as encryption can only be set during creation
let _servers = self.servers.read().await; let _servers = self.servers.read().await;
// TODO: Implement encryption setting for existing databases // TODO: Implement encryption setting for existing databases

View File

@@ -290,7 +290,11 @@ async fn test_02_strings_and_expiry() {
let ex0 = send_cmd(&mut s, &["EXISTS", "user:1"]).await; let ex0 = send_cmd(&mut s, &["EXISTS", "user:1"]).await;
assert_contains(&ex0, "0", "EXISTS after DEL"); assert_contains(&ex0, "0", "EXISTS after DEL");
// DEL non-existent should return 0
let del0 = send_cmd(&mut s, &["DEL", "user:1"]).await;
assert_contains(&del0, "0", "DEL user:1 when not exists -> 0");
// INCR behavior // INCR behavior
let i1 = send_cmd(&mut s, &["INCR", "count"]).await; let i1 = send_cmd(&mut s, &["INCR", "count"]).await;
assert_contains(&i1, "1", "INCR new key -> 1"); assert_contains(&i1, "1", "INCR new key -> 1");
@@ -602,7 +606,7 @@ async fn test_08_age_persistent_named_suite() {
// AGE LIST // AGE LIST
let lst = send_cmd(&mut s, &["AGE", "LIST"]).await; let lst = send_cmd(&mut s, &["AGE", "LIST"]).await;
assert_contains(&lst, "encpub", "AGE LIST label encpub"); // After flattening, LIST returns a flat array of managed key names
assert_contains(&lst, "app1", "AGE LIST includes app1"); assert_contains(&lst, "app1", "AGE LIST includes app1");
} }