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 keys_array = find_keys_array_mut(&mut doc).ok_or(
38 "API key configuration section not found in config file (expected [auth.api_key])",
39 )?;
40
41 let mut found = false;
43 let mut indices_to_remove = Vec::new();
44
45 for (index, item) in keys_array.iter().enumerate() {
46 if let Some(key_value) = item.as_str() {
47 if key_value == key_to_revoke {
54 indices_to_remove.push(index);
55 found = true;
56 break;
57 }
58
59 if key_value.starts_with("$argon2") {
63 let parts: Vec<&str> = key_value.split('$').collect();
64 if parts.len() >= 6 {
65 let salt = parts[4];
66 let hash = parts[5];
67 if salt == key_to_revoke || hash == key_to_revoke {
68 indices_to_remove.push(index);
69 found = true;
70 break;
71 }
72 }
73 }
74 }
75 }
76
77 if !found {
78 println!("Key not found in configuration: {key_to_revoke}");
79 println!();
80 println!("Tip: Use 'crates-docs list-api-keys' to see all configured keys.");
81 return Err("Key not found".into());
82 }
83
84 indices_to_remove.sort_unstable();
86 for index in indices_to_remove.iter().rev() {
87 keys_array.remove(*index);
88 }
89
90 let new_content = doc.to_string();
92 fs::write(config_path, new_content)
93 .map_err(|e| format!("Failed to write configuration file: {e}"))?;
94
95 println!("API key revoked successfully!");
96 println!();
97 println!(
98 "Removed {} key(s) from: {}",
99 indices_to_remove.len(),
100 config_path.display()
101 );
102 println!();
103 println!(
104 "Note: If the server is running, you may need to restart it for changes to take effect."
105 );
106 println!(" Or use hot-reload feature if available.");
107
108 Ok(())
109}
110
111fn find_keys_array_mut(doc: &mut toml_edit::DocumentMut) -> Option<&mut toml_edit::Array> {
116 let in_auth = doc
118 .get("auth")
119 .and_then(toml_edit::Item::as_table)
120 .and_then(|t| t.get("api_key"))
121 .and_then(toml_edit::Item::as_table)
122 .and_then(|t| t.get("keys"))
123 .and_then(toml_edit::Item::as_array)
124 .is_some();
125
126 if in_auth {
127 return doc
128 .get_mut("auth")
129 .and_then(toml_edit::Item::as_table_mut)
130 .and_then(|t| t.get_mut("api_key"))
131 .and_then(toml_edit::Item::as_table_mut)
132 .and_then(|t| t.get_mut("keys"))
133 .and_then(toml_edit::Item::as_array_mut);
134 }
135
136 doc.get_mut("api_key")
138 .and_then(toml_edit::Item::as_table_mut)
139 .and_then(|t| t.get_mut("keys"))
140 .and_then(toml_edit::Item::as_array_mut)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use std::io::Write;
147 use tempfile::NamedTempFile;
148
149 #[test]
150 fn test_revoke_api_key_removes_key() {
151 let mut temp_file = NamedTempFile::new().unwrap();
152 let content = r#"
153[server]
154host = "127.0.0.1"
155port = 8080
156
157[auth.api_key]
158enabled = true
159keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
160header_name = "X-API-Key"
161"#;
162 temp_file.write_all(content.as_bytes()).unwrap();
163
164 let path = temp_file.path();
165 let result =
166 run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
167 assert!(result.is_ok());
168
169 let new_content = std::fs::read_to_string(path).unwrap();
171 assert!(new_content.contains("hash2"));
172 assert!(!new_content.contains("hash1"));
173 }
174
175 #[test]
176 fn test_revoke_api_key_preserves_comments() {
177 let mut temp_file = NamedTempFile::new().unwrap();
178 let content = r#"
179[server]
180# Server configuration
181host = "127.0.0.1"
182port = 8080
183
184[auth.api_key]
185# API key configuration
186enabled = true
187# List of API key hashes
188keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
189header_name = "X-API-Key"
190"#;
191 temp_file.write_all(content.as_bytes()).unwrap();
192
193 let path = temp_file.path();
194 let result =
195 run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
196 assert!(result.is_ok());
197
198 let new_content = std::fs::read_to_string(path).unwrap();
200 assert!(new_content.contains("# Server configuration"));
201 assert!(new_content.contains("# API key configuration"));
202 assert!(new_content.contains("# List of API key hashes"));
203 }
204
205 #[test]
206 fn test_revoke_api_key_not_found() {
207 let mut temp_file = NamedTempFile::new().unwrap();
208 let content = r#"
209[auth.api_key]
210enabled = true
211keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1"]
212"#;
213 temp_file.write_all(content.as_bytes()).unwrap();
214
215 let path = temp_file.path();
216 let result = run_revoke_api_key_command(path, "nonexistent_key");
217 assert!(result.is_err());
218 }
219
220 #[test]
221 fn test_revoke_api_key_file_not_found() {
222 let result = run_revoke_api_key_command(Path::new("/nonexistent/config.toml"), "key");
223 assert!(result.is_err());
224 }
225
226 #[test]
227 fn test_revoke_api_key_legacy_top_level_section() {
228 let mut temp_file = NamedTempFile::new().unwrap();
229 let content = r#"
230[api_key]
231enabled = true
232keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
233"#;
234 temp_file.write_all(content.as_bytes()).unwrap();
235 let path = temp_file.path();
236 let result =
237 run_revoke_api_key_command(path, "$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1");
238 assert!(result.is_ok());
239 let new_content = std::fs::read_to_string(path).unwrap();
240 assert!(new_content.contains("hash2"));
241 assert!(!new_content.contains("hash1"));
242 }
243
244 #[test]
245 fn test_revoke_api_key_substring_does_not_match() {
246 let mut temp_file = NamedTempFile::new().unwrap();
248 let content = r#"
249[auth.api_key]
250enabled = true
251keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1"]
252"#;
253 temp_file.write_all(content.as_bytes()).unwrap();
254 let path = temp_file.path();
255 let result = run_revoke_api_key_command(path, "argon2id");
257 assert!(result.is_err());
258 let new_content = std::fs::read_to_string(path).unwrap();
259 assert!(new_content.contains("hash1"));
260 }
261
262 #[test]
263 fn test_revoke_api_key_by_salt_segment() {
264 let mut temp_file = NamedTempFile::new().unwrap();
265 let content = r#"
266[auth.api_key]
267enabled = true
268keys = ["$argon2id$v=19$m=47104,t=1,p=1$c2FsdA$hash1", "$argon2id$v=19$m=47104,t=1,p=1$c2FsdB$hash2"]
269"#;
270 temp_file.write_all(content.as_bytes()).unwrap();
271 let path = temp_file.path();
272 let result = run_revoke_api_key_command(path, "c2FsdB");
274 assert!(result.is_ok());
275 let new_content = std::fs::read_to_string(path).unwrap();
276 assert!(new_content.contains("hash1"));
277 assert!(!new_content.contains("hash2"));
278 }
279}