Skip to main content

crates_docs/cli/
list_api_keys_cmd.rs

1//! List API keys command implementation
2
3use crate::config::AppConfig;
4use std::path::Path;
5
6/// Truncate a long hash to `<prefix>...<suffix>` for display, counting by
7/// characters so the slice never lands inside a multi-byte UTF-8 sequence.
8///
9/// Byte-index slicing (`&hash[..30]`) panicked when a key hash loaded from a
10/// user-supplied `config.toml` contained a multi-byte character straddling the
11/// cut point. Hashes are normally ASCII (argon2/hex), where this is identical
12/// to the previous behaviour, but operator-edited config must never crash the
13/// audit command.
14fn truncate_hash_for_display(hash: &str) -> String {
15    const PREFIX_CHARS: usize = 30;
16    const SUFFIX_CHARS: usize = 20;
17    let char_count = hash.chars().count();
18    if char_count > PREFIX_CHARS + SUFFIX_CHARS + 10 {
19        let prefix: String = hash.chars().take(PREFIX_CHARS).collect();
20        let suffix: String = hash.chars().skip(char_count - SUFFIX_CHARS).collect();
21        format!("{prefix}...{suffix}")
22    } else {
23        hash.to_string()
24    }
25}
26
27/// List API keys from configuration file.
28///
29/// Reads the configuration file and displays all configured API key hashes.
30/// This helps operators audit which keys are currently active.
31///
32/// # Errors
33///
34/// Returns an error if the configuration file cannot be read or parsed.
35pub fn run_list_api_keys_command(config_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
36    // Check if config file exists
37    if !config_path.exists() {
38        eprintln!("Configuration file not found: {}", config_path.display());
39        eprintln!("No API keys are configured.");
40        return Ok(());
41    }
42
43    // Load configuration
44    let config = AppConfig::from_file(config_path).map_err(|e| {
45        format!(
46            "Failed to load configuration from {}: {}",
47            config_path.display(),
48            e
49        )
50    })?;
51
52    println!("API Key Configuration");
53    println!("=====================");
54    println!();
55
56    if !config.auth.api_key.enabled {
57        println!("Status: DISABLED");
58        println!();
59        println!("API key authentication is not enabled.");
60        println!("Set enabled = true in [auth.api_key] section to enable.");
61        return Ok(());
62    }
63
64    println!("Status: ENABLED");
65    println!();
66
67    if config.auth.api_key.keys.is_empty() {
68        println!("No API keys configured.");
69        println!("Use 'crates-docs generate-api-key' to create a new key.");
70    } else {
71        println!("Configured API keys ({}):", config.auth.api_key.keys.len());
72        println!();
73
74        for (index, key_hash) in config.auth.api_key.keys.iter().enumerate() {
75            let key_type = if key_hash.starts_with("legacy:") {
76                "Legacy Hash"
77            } else if key_hash.starts_with("$argon2") {
78                "Argon2 Hash"
79            } else {
80                "Plaintext (Insecure)"
81            };
82
83            println!("  [{}] {}", index + 1, key_type);
84
85            // Show a truncated version of the hash for identification
86            println!("      {}", truncate_hash_for_display(key_hash));
87            println!();
88        }
89
90        println!("Configuration:");
91        println!("  Header name: {}", config.auth.api_key.header_name);
92        println!("  Query param: {}", config.auth.api_key.query_param_name);
93        println!(
94            "  Allow query param: {}",
95            config.auth.api_key.allow_query_param
96        );
97        println!("  Key prefix: {}", config.auth.api_key.key_prefix);
98    }
99
100    println!();
101    println!("File: {}", config_path.display());
102
103    Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::truncate_hash_for_display;
109
110    #[test]
111    fn short_hash_is_unchanged() {
112        let h = "$argon2id$v=19$short";
113        assert_eq!(truncate_hash_for_display(h), h);
114    }
115
116    #[test]
117    fn long_ascii_hash_is_elided() {
118        let h = "a".repeat(80);
119        let out = truncate_hash_for_display(&h);
120        assert_eq!(out, format!("{}...{}", "a".repeat(30), "a".repeat(20)));
121        assert!(out.contains("..."));
122    }
123
124    #[test]
125    fn long_multibyte_hash_does_not_panic_and_stays_valid_utf8() {
126        // A multi-byte char straddling the old byte cut points (30 / len-20)
127        // previously panicked with "not a char boundary".
128        let h = format!(
129            "{}{}{}",
130            "a".repeat(28),
131            "\u{597d}".repeat(20),
132            "b".repeat(40)
133        );
134        let out = truncate_hash_for_display(&h);
135        // No panic; result is valid UTF-8 and elided.
136        assert!(out.contains("..."));
137        assert!(out.starts_with("aaaa"));
138        assert!(out.ends_with("bbbb"));
139    }
140}