Skip to main content

crates_docs/cli/
revoke_api_key_cmd.rs

1//! Revoke API key command implementation
2
3use std::fs;
4use std::path::Path;
5
6/// Revoke an API key from configuration file.
7///
8/// Removes the specified key hash or key ID from the configuration file.
9/// The configuration file format and comments are preserved.
10///
11/// # Errors
12///
13/// Returns an error if:
14/// - The configuration file cannot be read or written
15/// - The specified key is not found
16/// - The configuration file format is invalid
17pub fn run_revoke_api_key_command(
18    config_path: &Path,
19    key_to_revoke: &str,
20) -> Result<(), Box<dyn std::error::Error>> {
21    // Check if config file exists
22    if !config_path.exists() {
23        return Err(format!("Configuration file not found: {}", config_path.display()).into());
24    }
25
26    // Read the configuration file content
27    let content = fs::read_to_string(config_path)
28        .map_err(|e| format!("Failed to read configuration file: {e}"))?;
29
30    // Parse as TOML document (preserves comments and formatting)
31    let mut doc = content
32        .parse::<toml_edit::DocumentMut>()
33        .map_err(|e| format!("Failed to parse configuration file: {e}"))?;
34
35    // Get the api_key table
36    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    // Get the keys array
42    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    // Find and remove the key
48    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            // Check if this is the key we want to revoke
54            // Match by:
55            // 1. Exact hash match
56            // 2. Key ID match (extracted from hash)
57            // 3. Partial match (for convenience)
58
59            if key_value == key_to_revoke {
60                indices_to_remove.push(index);
61                found = true;
62                break;
63            }
64
65            // Try to extract key_id from the hash
66            // Argon2 hashes have format: $argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>
67            // We can try to match by partial content
68            if key_value.contains(key_to_revoke) {
69                indices_to_remove.push(index);
70                found = true;
71                break;
72            }
73
74            // Try to match by salt portion (which might be used as key_id)
75            // Extract salt from Argon2 PHC format
76            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    // Remove the key(s) - remove from highest index first to maintain validity
98    indices_to_remove.sort_unstable();
99    for index in indices_to_remove.iter().rev() {
100        keys_array.remove(*index);
101    }
102
103    // Write back to file
104    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        // Verify the key was removed
151        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        // Verify comments are preserved
180        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}