use std::fs;
use std::path::Path;
pub fn run_revoke_api_key_command(
config_path: &Path,
key_to_revoke: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if !config_path.exists() {
return Err(format!("Configuration file not found: {}", config_path.display()).into());
}
let content = fs::read_to_string(config_path)
.map_err(|e| format!("Failed to read configuration file: {e}"))?;
let mut doc = content
.parse::<toml_edit::DocumentMut>()
.map_err(|e| format!("Failed to parse configuration file: {e}"))?;
let keys_array = find_keys_array_mut(&mut doc).ok_or(
"API key configuration section not found in config file (expected [auth.api_key])",
)?;
let mut found = false;
let mut indices_to_remove = Vec::new();
for (index, item) in keys_array.iter().enumerate() {
if let Some(key_value) = item.as_str() {
if key_value == key_to_revoke {
indices_to_remove.push(index);
found = true;
break;
}
if key_value.starts_with("$argon2") {
let parts: Vec<&str> = key_value.split('$').collect();
if parts.len() >= 6 {
let salt = parts[4];
let hash = parts[5];
if salt == key_to_revoke || hash == key_to_revoke {
indices_to_remove.push(index);
found = true;
break;
}
}
}
}
}
if !found {
println!("Key not found in configuration: {key_to_revoke}");
println!();
println!("Tip: Use 'crates-docs list-api-keys' to see all configured keys.");
return Err("Key not found".into());
}
indices_to_remove.sort_unstable();
for index in indices_to_remove.iter().rev() {
keys_array.remove(*index);
}
let new_content = doc.to_string();
fs::write(config_path, new_content)
.map_err(|e| format!("Failed to write configuration file: {e}"))?;
println!("API key revoked successfully!");
println!();
println!(
"Removed {} key(s) from: {}",
indices_to_remove.len(),
config_path.display()
);
println!();
println!(
"Note: If the server is running, you may need to restart it for changes to take effect."
);
println!(" Or use hot-reload feature if available.");
Ok(())
}
fn find_keys_array_mut(doc: &mut toml_edit::DocumentMut) -> Option<&mut toml_edit::Array> {
let in_auth = doc
.get("auth")
.and_then(toml_edit::Item::as_table)
.and_then(|t| t.get("api_key"))
.and_then(toml_edit::Item::as_table)
.and_then(|t| t.get("keys"))
.and_then(toml_edit::Item::as_array)
.is_some();
if in_auth {
return doc
.get_mut("auth")
.and_then(toml_edit::Item::as_table_mut)
.and_then(|t| t.get_mut("api_key"))
.and_then(toml_edit::Item::as_table_mut)
.and_then(|t| t.get_mut("keys"))
.and_then(toml_edit::Item::as_array_mut);
}
doc.get_mut("api_key")
.and_then(toml_edit::Item::as_table_mut)
.and_then(|t| t.get_mut("keys"))
.and_then(toml_edit::Item::as_array_mut)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_revoke_api_key_removes_key() {
let mut temp_file = NamedTempFile::new().unwrap();
let content = r#"
[server]
host = "127.0.0.1"
port = 8080
[auth.api_key]
enabled = true
keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
header_name = "X-API-Key"
"#;
temp_file.write_all(content.as_bytes()).unwrap();
let path = temp_file.path();
let result =
run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
assert!(result.is_ok());
let new_content = std::fs::read_to_string(path).unwrap();
assert!(new_content.contains("hash2"));
assert!(!new_content.contains("hash1"));
}
#[test]
fn test_revoke_api_key_preserves_comments() {
let mut temp_file = NamedTempFile::new().unwrap();
let content = r#"
[server]
# Server configuration
host = "127.0.0.1"
port = 8080
[auth.api_key]
# API key configuration
enabled = true
# List of API key hashes
keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
header_name = "X-API-Key"
"#;
temp_file.write_all(content.as_bytes()).unwrap();
let path = temp_file.path();
let result =
run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
assert!(result.is_ok());
let new_content = std::fs::read_to_string(path).unwrap();
assert!(new_content.contains("# Server configuration"));
assert!(new_content.contains("# API key configuration"));
assert!(new_content.contains("# List of API key hashes"));
}
#[test]
fn test_revoke_api_key_not_found() {
let mut temp_file = NamedTempFile::new().unwrap();
let content = r#"
[auth.api_key]
enabled = true
keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1"]
"#;
temp_file.write_all(content.as_bytes()).unwrap();
let path = temp_file.path();
let result = run_revoke_api_key_command(path, "nonexistent_key");
assert!(result.is_err());
}
#[test]
fn test_revoke_api_key_file_not_found() {
let result = run_revoke_api_key_command(Path::new("/nonexistent/config.toml"), "key");
assert!(result.is_err());
}
#[test]
fn test_revoke_api_key_legacy_top_level_section() {
let mut temp_file = NamedTempFile::new().unwrap();
let content = r#"
[api_key]
enabled = true
keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
"#;
temp_file.write_all(content.as_bytes()).unwrap();
let path = temp_file.path();
let result =
run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
assert!(result.is_ok());
let new_content = std::fs::read_to_string(path).unwrap();
assert!(new_content.contains("hash2"));
assert!(!new_content.contains("hash1"));
}
#[test]
fn test_revoke_api_key_substring_does_not_match() {
let mut temp_file = NamedTempFile::new().unwrap();
let content = r#"
[auth.api_key]
enabled = true
keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1"]
"#;
temp_file.write_all(content.as_bytes()).unwrap();
let path = temp_file.path();
let result = run_revoke_api_key_command(path, "argon2id");
assert!(result.is_err());
let new_content = std::fs::read_to_string(path).unwrap();
assert!(new_content.contains("hash1"));
}
#[test]
fn test_revoke_api_key_by_salt_segment() {
let mut temp_file = NamedTempFile::new().unwrap();
let content = r#"
[auth.api_key]
enabled = true
keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
"#;
temp_file.write_all(content.as_bytes()).unwrap();
let path = temp_file.path();
let result = run_revoke_api_key_command(path, "c2FsdB");
assert!(result.is_ok());
let new_content = std::fs::read_to_string(path).unwrap();
assert!(new_content.contains("hash1"));
assert!(!new_content.contains("hash2"));
}
}