apcore_cli/security/
auth.rs1use thiserror::Error;
5
6use crate::config::ConfigResolver;
7use crate::security::config_encryptor::ConfigEncryptor;
8
9#[derive(Debug, Error)]
15pub enum AuthenticationError {
16 #[error(
18 "Remote registry requires authentication. \
19 Set --api-key, APCORE_AUTH_API_KEY, or auth.api_key in config."
20 )]
21 MissingApiKey,
22
23 #[error("Authentication failed. Verify your API key.")]
25 InvalidApiKey,
26
27 #[error("keyring error: {0}")]
29 KeyringError(String),
30
31 #[error("authentication request failed: {0}")]
33 RequestError(String),
34}
35
36pub struct AuthProvider {
47 config: ConfigResolver,
48}
49
50impl AuthProvider {
51 pub fn new(config: ConfigResolver) -> Self {
53 Self { config }
54 }
55
56 pub fn get_api_key(&self) -> Option<String> {
60 if let Ok(val) = std::env::var("APCORE_AUTH_API_KEY") {
62 if !val.is_empty() {
63 return Some(val);
64 }
65 }
66
67 let raw = self
71 .config
72 .resolve("auth.api_key", Some("--api-key"), None)?;
73
74 if raw.starts_with("keyring:") || raw.starts_with("enc:") {
76 let encryptor = ConfigEncryptor::new().ok()?;
77 encryptor
78 .retrieve(&raw, "auth.api_key")
79 .map_err(|e| {
80 tracing::warn!("Failed to decode auth.api_key: {e}");
81 })
82 .ok()
83 } else {
84 Some(raw)
85 }
86 }
87
88 pub fn authenticate_request(
93 &self,
94 builder: reqwest::RequestBuilder,
95 ) -> Result<reqwest::RequestBuilder, AuthenticationError> {
96 let key = self
97 .get_api_key()
98 .ok_or(AuthenticationError::MissingApiKey)?;
99 Ok(builder.header("Authorization", format!("Bearer {key}")))
100 }
101
102 pub fn check_status_code(&self, status: u16) -> Result<(), AuthenticationError> {
107 match status {
108 401 | 403 => Err(AuthenticationError::InvalidApiKey),
109 _ => Ok(()),
110 }
111 }
112
113 pub fn handle_response(
117 &self,
118 response: reqwest::Response,
119 ) -> Result<reqwest::Response, AuthenticationError> {
120 self.check_status_code(response.status().as_u16())?;
121 Ok(response)
122 }
123}
124
125#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::sync::Mutex;
133
134 static ENV_LOCK: Mutex<()> = Mutex::new(());
136
137 fn make_resolver_with_key(key: &str) -> ConfigResolver {
138 let mut flags = std::collections::HashMap::new();
140 flags.insert("--api-key".to_string(), Some(key.to_string()));
141 ConfigResolver::new(Some(flags), None)
142 }
143
144 fn make_resolver_empty() -> ConfigResolver {
145 ConfigResolver::new(None, None)
146 }
147
148 #[test]
149 fn test_get_api_key_from_env_var() {
150 let _guard = ENV_LOCK.lock().unwrap();
151 unsafe { std::env::set_var("APCORE_AUTH_API_KEY", "test-key-env") };
153 let provider = AuthProvider::new(make_resolver_empty());
154 let result = provider.get_api_key();
155 unsafe { std::env::remove_var("APCORE_AUTH_API_KEY") };
156 assert_eq!(result, Some("test-key-env".to_string()));
157 }
158
159 #[test]
160 fn test_get_api_key_none_when_not_configured() {
161 let _guard = ENV_LOCK.lock().unwrap();
162 unsafe { std::env::remove_var("APCORE_AUTH_API_KEY") };
163 let provider = AuthProvider::new(make_resolver_empty());
164 let result = provider.get_api_key();
165 assert_eq!(result, None);
166 }
167
168 #[test]
169 fn test_get_api_key_plain_key_from_cli_flag() {
170 let _guard = ENV_LOCK.lock().unwrap();
171 unsafe { std::env::remove_var("APCORE_AUTH_API_KEY") };
172 let provider = AuthProvider::new(make_resolver_with_key("my-plain-key"));
173 let result = provider.get_api_key();
174 assert_eq!(result, Some("my-plain-key".to_string()));
175 }
176
177 #[test]
178 fn test_authenticate_request_adds_bearer_header() {
179 let _guard = ENV_LOCK.lock().unwrap();
180 unsafe { std::env::set_var("APCORE_AUTH_API_KEY", "abc123") };
181 let provider = AuthProvider::new(make_resolver_empty());
182 let client = reqwest::Client::new();
183 let builder = client.get("https://example.com");
184 let result = provider.authenticate_request(builder);
185 unsafe { std::env::remove_var("APCORE_AUTH_API_KEY") };
186 assert!(result.is_ok());
187 }
188
189 #[test]
190 fn test_authenticate_request_no_key_raises() {
191 let _guard = ENV_LOCK.lock().unwrap();
192 unsafe { std::env::remove_var("APCORE_AUTH_API_KEY") };
193 let provider = AuthProvider::new(make_resolver_empty());
194 let client = reqwest::Client::new();
195 let builder = client.get("https://example.com");
196 let result = provider.authenticate_request(builder);
197 assert!(matches!(result, Err(AuthenticationError::MissingApiKey)));
198 }
199
200 #[test]
201 fn test_handle_response_401_returns_invalid_api_key() {
202 let missing = AuthenticationError::MissingApiKey;
210 assert_eq!(
211 missing.to_string(),
212 "Remote registry requires authentication. \
213 Set --api-key, APCORE_AUTH_API_KEY, or auth.api_key in config."
214 );
215
216 let invalid = AuthenticationError::InvalidApiKey;
217 assert_eq!(
218 invalid.to_string(),
219 "Authentication failed. Verify your API key."
220 );
221 }
222
223 #[test]
224 fn test_handle_response_403_returns_invalid_api_key() {
225 let err = AuthenticationError::InvalidApiKey;
230 assert!(err.to_string().contains("Authentication failed"));
231 }
232}