fabryk_cli/
config_sections.rs1use fabryk_core::{Error, Result};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
28pub struct TlsConfig {
29 #[serde(default)]
31 pub cert_path: Option<String>,
32
33 #[serde(default)]
35 pub key_path: Option<String>,
36}
37
38impl TlsConfig {
39 pub fn enabled(&self) -> bool {
41 self.cert_path.as_ref().is_some_and(|p| !p.is_empty())
42 && self.key_path.as_ref().is_some_and(|p| !p.is_empty())
43 }
44
45 pub fn validate(&self) -> Result<()> {
53 let has_cert = self.cert_path.as_ref().is_some_and(|p| !p.is_empty());
54 let has_key = self.key_path.as_ref().is_some_and(|p| !p.is_empty());
55
56 if has_cert != has_key {
57 return Err(Error::config(
58 "tls.cert_path and tls.key_path must both be set (or both unset). \
59 Only one was provided.",
60 ));
61 }
62
63 if has_cert && has_key {
64 let cert_path = self.cert_path.as_ref().unwrap();
65 let key_path = self.key_path.as_ref().unwrap();
66
67 if !std::path::Path::new(cert_path).exists() {
68 return Err(Error::config(format!(
69 "tls.cert_path '{cert_path}' does not exist"
70 )));
71 }
72 if !std::path::Path::new(key_path).exists() {
73 return Err(Error::config(format!(
74 "tls.key_path '{key_path}' does not exist"
75 )));
76 }
77
78 log::info!("TLS enabled — cert: {cert_path}, key: {key_path}");
79 }
80
81 Ok(())
82 }
83}
84
85fn default_jwks_url() -> String {
91 "https://www.googleapis.com/oauth2/v3/certs".to_string()
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct OAuthConfig {
109 #[serde(default)]
111 pub enabled: bool,
112
113 #[serde(default)]
115 pub client_id: String,
116
117 #[serde(default)]
119 pub domain: String,
120
121 #[serde(default = "default_jwks_url")]
123 pub jwks_url: String,
124}
125
126impl Default for OAuthConfig {
127 fn default() -> Self {
128 Self {
129 enabled: false,
130 client_id: String::new(),
131 domain: String::new(),
132 jwks_url: default_jwks_url(),
133 }
134 }
135}
136
137impl OAuthConfig {
138 pub fn validate(&self, env_prefix: &str) -> Result<()> {
146 if !self.enabled {
147 return Ok(());
148 }
149
150 if self.client_id.is_empty() {
151 return Err(Error::config(format!(
152 "oauth.client_id is required when oauth.enabled is true. \
153 Set {env_prefix}_OAUTH_CLIENT_ID or [oauth] client_id in config file."
154 )));
155 }
156
157 if self.domain.is_empty() {
158 log::warn!(
159 "oauth.domain is not set — any Google account can authenticate. \
160 Set {env_prefix}_OAUTH_DOMAIN to restrict access."
161 );
162 }
163
164 Ok(())
165 }
166}
167
168#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
179 fn test_tls_default_disabled() {
180 let tls = TlsConfig::default();
181 assert!(!tls.enabled());
182 }
183
184 #[test]
185 fn test_tls_enabled_both_set() {
186 let tls = TlsConfig {
187 cert_path: Some("/path/to/cert.pem".to_string()),
188 key_path: Some("/path/to/key.pem".to_string()),
189 };
190 assert!(tls.enabled());
191 }
192
193 #[test]
194 fn test_tls_disabled_empty_strings() {
195 let tls = TlsConfig {
196 cert_path: Some(String::new()),
197 key_path: Some(String::new()),
198 };
199 assert!(!tls.enabled());
200 }
201
202 #[test]
203 fn test_tls_disabled_only_cert() {
204 let tls = TlsConfig {
205 cert_path: Some("/path/cert.pem".to_string()),
206 key_path: None,
207 };
208 assert!(!tls.enabled());
209 }
210
211 #[test]
212 fn test_tls_validate_both_unset_ok() {
213 let tls = TlsConfig::default();
214 assert!(tls.validate().is_ok());
215 }
216
217 #[test]
218 fn test_tls_validate_asymmetric_error() {
219 let tls = TlsConfig {
220 cert_path: Some("/path/cert.pem".to_string()),
221 key_path: None,
222 };
223 let err = tls.validate().unwrap_err();
224 assert!(err.to_string().contains("both be set"));
225 }
226
227 #[test]
228 fn test_tls_validate_nonexistent_cert() {
229 let tls = TlsConfig {
230 cert_path: Some("/nonexistent/cert.pem".to_string()),
231 key_path: Some("/nonexistent/key.pem".to_string()),
232 };
233 let err = tls.validate().unwrap_err();
234 assert!(err.to_string().contains("does not exist"));
235 }
236
237 #[test]
238 fn test_tls_validate_existing_files() {
239 let dir = tempfile::TempDir::new().unwrap();
240 let cert = dir.path().join("cert.pem");
241 let key = dir.path().join("key.pem");
242 std::fs::write(&cert, "cert").unwrap();
243 std::fs::write(&key, "key").unwrap();
244
245 let tls = TlsConfig {
246 cert_path: Some(cert.to_str().unwrap().to_string()),
247 key_path: Some(key.to_str().unwrap().to_string()),
248 };
249 assert!(tls.validate().is_ok());
250 }
251
252 #[test]
255 fn test_oauth_default_disabled() {
256 let oauth = OAuthConfig::default();
257 assert!(!oauth.enabled);
258 assert!(oauth.client_id.is_empty());
259 assert!(!oauth.jwks_url.is_empty());
260 }
261
262 #[test]
263 fn test_oauth_validate_disabled_ok() {
264 let oauth = OAuthConfig::default();
265 assert!(oauth.validate("APP").is_ok());
266 }
267
268 #[test]
269 fn test_oauth_validate_enabled_no_client_id() {
270 let oauth = OAuthConfig {
271 enabled: true,
272 ..Default::default()
273 };
274 let err = oauth.validate("APP").unwrap_err();
275 assert!(err.to_string().contains("client_id"));
276 }
277
278 #[test]
279 fn test_oauth_validate_enabled_with_client_id() {
280 let oauth = OAuthConfig {
281 enabled: true,
282 client_id: "my-client-id".to_string(),
283 domain: "example.com".to_string(),
284 ..Default::default()
285 };
286 assert!(oauth.validate("APP").is_ok());
287 }
288
289 #[test]
290 fn test_oauth_default_jwks_url() {
291 let oauth = OAuthConfig::default();
292 assert!(oauth.jwks_url.contains("googleapis.com"));
293 }
294
295 #[test]
298 fn test_tls_config_serde_roundtrip() {
299 let tls = TlsConfig {
300 cert_path: Some("/cert.pem".to_string()),
301 key_path: Some("/key.pem".to_string()),
302 };
303 let toml_str = toml::to_string_pretty(&tls).unwrap();
304 let parsed: TlsConfig = toml::from_str(&toml_str).unwrap();
305 assert_eq!(parsed.cert_path, tls.cert_path);
306 assert_eq!(parsed.key_path, tls.key_path);
307 }
308
309 #[test]
310 fn test_oauth_config_serde_roundtrip() {
311 let oauth = OAuthConfig {
312 enabled: true,
313 client_id: "test-id".to_string(),
314 domain: "test.com".to_string(),
315 ..Default::default()
316 };
317 let toml_str = toml::to_string_pretty(&oauth).unwrap();
318 let parsed: OAuthConfig = toml::from_str(&toml_str).unwrap();
319 assert_eq!(parsed.enabled, oauth.enabled);
320 assert_eq!(parsed.client_id, oauth.client_id);
321 assert_eq!(parsed.domain, oauth.domain);
322 assert_eq!(parsed.jwks_url, oauth.jwks_url);
323 }
324}