1#![allow(clippy::missing_errors_doc)]
2
3fn pick(env_key: Option<String>, keychain_key: Option<String>) -> Option<String> {
18 env_key
19 .filter(|k| !k.trim().is_empty())
20 .or(keychain_key.filter(|k| !k.trim().is_empty()))
21 .map(|k| k.trim().to_string())
22}
23
24#[cfg(not(target_os = "macos"))]
25pub fn resolve_api_key() -> anyhow::Result<String> {
26 pick(std::env::var("REDASH_API_KEY").ok(), None)
27 .ok_or_else(|| anyhow::anyhow!("REDASH_API_KEY environment variable not set"))
28}
29
30#[cfg(not(target_os = "macos"))]
31pub fn login() -> anyhow::Result<()> {
32 anyhow::bail!(
33 "`stmo-cli login` stores the key in the macOS Keychain and is only available on \
34 macOS. On this platform, set the REDASH_API_KEY environment variable instead."
35 )
36}
37
38#[cfg(target_os = "macos")]
39mod macos {
40 use super::pick;
41 use anyhow::{Context, Result, bail};
42 use std::io::IsTerminal;
43 use std::process::Command;
44
45 const KEYCHAIN_SERVICE: &str = "stmo-cli";
46
47 fn keychain_file() -> Option<String> {
52 std::env::var("STMO_KEYCHAIN_PATH")
53 .ok()
54 .filter(|p| !p.trim().is_empty())
55 }
56
57 fn with_keychain(base: &[&str]) -> Vec<String> {
60 let mut args: Vec<String> = base.iter().map(|s| (*s).to_string()).collect();
61 if let Some(path) = keychain_file() {
62 args.push(path);
63 }
64 args
65 }
66
67 pub fn resolve_api_key() -> Result<String> {
68 if let Some(k) = pick(std::env::var("REDASH_API_KEY").ok(), None) {
69 return Ok(k);
70 }
71 if let Some(k) = pick(None, read_from_keychain()?) {
72 return Ok(k);
73 }
74 if std::io::stderr().is_terminal() {
75 store_in_keychain()?;
76 if let Some(k) = pick(None, read_from_keychain()?) {
77 return Ok(k);
78 }
79 }
80 bail!(
81 "REDASH_API_KEY is not set and no '{KEYCHAIN_SERVICE}' key is stored in the \
82 macOS Keychain.\nRun `stmo-cli login` in your own terminal once to store it."
83 )
84 }
85
86 pub fn login() -> Result<()> {
87 store_in_keychain()?;
88 read_from_keychain()?;
90 println!("Stored the Redash API key in the macOS Keychain (service '{KEYCHAIN_SERVICE}').");
91 Ok(())
92 }
93
94 fn read_from_keychain() -> Result<Option<String>> {
95 let output = Command::new("security")
96 .args(with_keychain(&[
97 "find-generic-password",
98 "-s",
99 KEYCHAIN_SERVICE,
100 "-w",
101 ]))
102 .output()
103 .context("Failed to run `security` to read the Redash API key from the Keychain")?;
104 if !output.status.success() {
105 return Ok(None);
106 }
107 let key = String::from_utf8(output.stdout)
108 .context("Keychain item was not valid UTF-8")?
109 .trim()
110 .to_string();
111 Ok((!key.is_empty()).then_some(key))
112 }
113
114 fn store_in_keychain() -> Result<()> {
115 let account = std::env::var("USER").unwrap_or_else(|_| KEYCHAIN_SERVICE.to_string());
116 eprintln!("Enter your Redash API key (https://sql.telemetry.mozilla.org/users/me):");
117 let status = Command::new("security")
118 .args(with_keychain(&[
119 "add-generic-password",
120 "-a",
121 &account,
122 "-s",
123 KEYCHAIN_SERVICE,
124 "-U",
125 "-w",
126 ]))
127 .status()
129 .context("Failed to run `security` to store the Redash API key")?;
130 if !status.success() {
131 bail!("Failed to store the Redash API key in the macOS Keychain");
132 }
133 Ok(())
134 }
135}
136
137#[cfg(target_os = "macos")]
138pub use macos::{login, resolve_api_key};
139
140#[cfg(test)]
141mod tests {
142 use super::pick;
143
144 #[test]
145 fn env_key_wins_over_keychain() {
146 assert_eq!(
147 pick(
148 Some("env-key".to_string()),
149 Some("keychain-key".to_string())
150 ),
151 Some("env-key".to_string())
152 );
153 }
154
155 #[test]
156 fn blank_env_key_falls_through_to_keychain() {
157 assert_eq!(
158 pick(Some(" ".to_string()), Some("keychain-key".to_string())),
159 Some("keychain-key".to_string())
160 );
161 }
162
163 #[test]
164 fn keychain_used_when_env_unset() {
165 assert_eq!(
166 pick(None, Some("keychain-key".to_string())),
167 Some("keychain-key".to_string())
168 );
169 }
170
171 #[test]
172 fn both_unset_or_blank_returns_none() {
173 assert_eq!(pick(None, None), None);
174 assert_eq!(pick(Some(String::new()), Some(" ".to_string())), None);
175 }
176
177 #[test]
178 fn result_is_trimmed() {
179 assert_eq!(
180 pick(Some(" env-key ".to_string()), None),
181 Some("env-key".to_string())
182 );
183 assert_eq!(
184 pick(None, Some(" keychain-key ".to_string())),
185 Some("keychain-key".to_string())
186 );
187 }
188}