crates_docs/cli/
revoke_api_key_cmd.rs1use std::fs;
4use std::path::Path;
5
6pub fn run_revoke_api_key_command(
18 config_path: &Path,
19 key_to_revoke: &str,
20) -> Result<(), Box<dyn std::error::Error>> {
21 if !config_path.exists() {
23 return Err(format!("Configuration file not found: {}", config_path.display()).into());
24 }
25
26 let content = fs::read_to_string(config_path)
28 .map_err(|e| format!("Failed to read configuration file: {e}"))?;
29
30 let mut doc = content
32 .parse::<toml_edit::DocumentMut>()
33 .map_err(|e| format!("Failed to parse configuration file: {e}"))?;
34
35 let api_key_table = doc
37 .get_mut("api_key")
38 .and_then(|item| item.as_table_mut())
39 .ok_or("API key configuration section not found in config file")?;
40
41 let keys_array = api_key_table
43 .get_mut("keys")
44 .and_then(|item| item.as_array_mut())
45 .ok_or("API keys array not found in configuration")?;
46
47 let mut found = false;
49 let mut indices_to_remove = Vec::new();
50
51 for (index, item) in keys_array.iter().enumerate() {
52 if let Some(key_value) = item.as_str() {
53 if key_value == key_to_revoke {
60 indices_to_remove.push(index);
61 found = true;
62 break;
63 }
64
65 if key_value.contains(key_to_revoke) {
69 indices_to_remove.push(index);
70 found = true;
71 break;
72 }
73
74 if key_value.starts_with("$argon2") {
77 let parts: Vec<&str> = key_value.split('$').collect();
78 if parts.len() >= 4 {
79 let salt = parts[3];
80 if salt == key_to_revoke {
81 indices_to_remove.push(index);
82 found = true;
83 break;
84 }
85 }
86 }
87 }
88 }
89
90 if !found {
91 println!("Key not found in configuration: {key_to_revoke}");
92 println!();
93 println!("Tip: Use 'crates-docs list-api-keys' to see all configured keys.");
94 return Err("Key not found".into());
95 }
96
97 indices_to_remove.sort_unstable();
99 for index in indices_to_remove.iter().rev() {
100 keys_array.remove(*index);
101 }
102
103 let new_content = doc.to_string();
105 fs::write(config_path, new_content)
106 .map_err(|e| format!("Failed to write configuration file: {e}"))?;
107
108 println!("API key revoked successfully!");
109 println!();
110 println!(
111 "Removed {} key(s) from: {}",
112 indices_to_remove.len(),
113 config_path.display()
114 );
115 println!();
116 println!(
117 "Note: If the server is running, you may need to restart it for changes to take effect."
118 );
119 println!(" Or use hot-reload feature if available.");
120
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::io::Write;
128 use tempfile::NamedTempFile;
129
130 #[test]
131 fn test_revoke_api_key_removes_key() {
132 let mut temp_file = NamedTempFile::new().unwrap();
133 let content = r#"
134[server]
135host = "127.0.0.1"
136port = 8080
137
138[api_key]
139enabled = true
140keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash2"]
141header_name = "X-API-Key"
142"#;
143 temp_file.write_all(content.as_bytes()).unwrap();
144
145 let path = temp_file.path();
146 let result =
147 run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
148 assert!(result.is_ok());
149
150 let new_content = std::fs::read_to_string(path).unwrap();
152 assert!(new_content.contains("hash2"));
153 assert!(!new_content.contains("hash1"));
154 }
155
156 #[test]
157 fn test_revoke_api_key_preserves_comments() {
158 let mut temp_file = NamedTempFile::new().unwrap();
159 let content = r#"
160[server]
161# Server configuration
162host = "127.0.0.1"
163port = 8080
164
165[api_key]
166# API key configuration
167enabled = true
168# List of API key hashes
169keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash2"]
170header_name = "X-API-Key"
171"#;
172 temp_file.write_all(content.as_bytes()).unwrap();
173
174 let path = temp_file.path();
175 let result =
176 run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
177 assert!(result.is_ok());
178
179 let new_content = std::fs::read_to_string(path).unwrap();
181 assert!(new_content.contains("# Server configuration"));
182 assert!(new_content.contains("# API key configuration"));
183 assert!(new_content.contains("# List of API key hashes"));
184 }
185
186 #[test]
187 fn test_revoke_api_key_not_found() {
188 let mut temp_file = NamedTempFile::new().unwrap();
189 let content = r#"
190[api_key]
191enabled = true
192keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1"]
193"#;
194 temp_file.write_all(content.as_bytes()).unwrap();
195
196 let path = temp_file.path();
197 let result = run_revoke_api_key_command(path, "nonexistent_key");
198 assert!(result.is_err());
199 }
200
201 #[test]
202 fn test_revoke_api_key_file_not_found() {
203 let result = run_revoke_api_key_command(Path::new("/nonexistent/config.toml"), "key");
204 assert!(result.is_err());
205 }
206}