manta_shared/common/config/
mod.rs1pub mod types;
2
3use std::{
4 fs::{self, File},
5 io::{Read, Write},
6 path::PathBuf,
7};
8
9use crate::common::error::MantaError as Error;
10use config::Config;
11use directories::ProjectDirs;
12use toml_edit::DocumentMut;
13
14fn get_project_dirs() -> Result<ProjectDirs, Error> {
20 ProjectDirs::from(
21 "local", "cscs", "manta", )
25 .ok_or_else(|| {
26 Error::MissingField(
27 "Could not determine project directories \
28 (home directory may not be set)"
29 .to_string(),
30 )
31 })
32}
33
34pub fn get_default_config_path() -> Result<PathBuf, Error> {
37 Ok(PathBuf::from(get_project_dirs()?.config_dir()))
38}
39
40pub fn get_default_manta_config_file_path() -> Result<PathBuf, Error> {
46 let mut path = get_default_config_path()?;
47 path.push("config.toml");
48 Ok(path)
49}
50
51pub fn get_default_manta_cli_config_file_path() -> Result<PathBuf, Error> {
54 let mut path = get_default_config_path()?;
55 path.push("cli.toml");
56 Ok(path)
57}
58
59pub fn get_default_manta_server_config_file_path() -> Result<PathBuf, Error> {
62 let mut path = get_default_config_path()?;
63 path.push("server.toml");
64 Ok(path)
65}
66
67pub fn get_default_cache_path() -> Result<PathBuf, Error> {
70 Ok(PathBuf::from(get_project_dirs()?.cache_dir()))
71}
72
73pub fn read_config_toml() -> Result<(PathBuf, DocumentMut), Error> {
79 let path = get_cli_config_file_path()?;
80
81 tracing::debug!(
82 "Reading manta CLI configuration from {}",
83 path.to_string_lossy()
84 );
85
86 let content = fs::read_to_string(&path)?;
87
88 let doc = content.parse::<DocumentMut>()?;
89
90 Ok((path, doc))
91}
92
93pub fn write_config_toml(
95 path: &std::path::Path,
96 doc: &DocumentMut,
97) -> Result<(), Error> {
98 let mut file = std::fs::OpenOptions::new()
99 .write(true)
100 .truncate(true)
101 .open(path)?;
102
103 file.write_all(doc.to_string().as_bytes())?;
104 file.flush()?;
105
106 Ok(())
107}
108
109pub fn get_csm_root_cert_content(file_path: &str) -> Result<Vec<u8>, Error> {
113 let mut buf = Vec::new();
114 let root_cert_file_rslt = File::open(file_path);
115
116 let file_rslt = if root_cert_file_rslt.is_err() {
117 let mut config_path = get_default_config_path()?;
118 config_path.push(file_path);
119 File::open(config_path)
120 } else {
121 root_cert_file_rslt
122 };
123
124 match file_rslt {
125 Ok(mut file) => {
126 file.read_to_end(&mut buf)?;
127 Ok(buf)
128 }
129 Err(_) => Err(Error::NotFound(
130 "CA public root file could not be found".to_string(),
131 )),
132 }
133}
134
135pub fn get_cli_config_file_path() -> Result<PathBuf, Error> {
137 if let Ok(env_path) = std::env::var("MANTA_CLI_CONFIG") {
138 Ok(PathBuf::from(env_path))
139 } else {
140 get_default_manta_cli_config_file_path()
141 }
142}
143
144pub fn get_server_config_file_path() -> Result<PathBuf, Error> {
146 if let Ok(env_path) = std::env::var("MANTA_SERVER_CONFIG") {
147 Ok(PathBuf::from(env_path))
148 } else {
149 get_default_manta_server_config_file_path()
150 }
151}
152
153const CLI_CONFIG_SAMPLE: &str = r#"log = "info"
155audit_file = "/tmp/manta-cli-audit.log"
156site = "<site_name>"
157parent_hsm_group = ""
158manta_server_url = "https://manta-server.example.com:8443"
159
160[sites.<site_name>]
161backend = "csm" # or "ochami"
162shasta_base_url = "https://api.example.com"
163root_ca_cert_file = "alps_root_cert.pem"
164"#;
165
166const CLI_CONFIG_MIGRATION: &str = "\
168Migration from ~/.config/manta/config.toml:
169 copy these fields verbatim: log, audit_file, site, parent_hsm_group,
170 auditor, sites
171 add CLI-only (now required): manta_server_url = \"https://...\"
172 (CLI talks only to the manta server)
173 drop (no longer recognised): sites.<X>.manta_server_url
174 do not copy (server-only fields): the [server] section belongs in
175 server.toml, not cli.toml";
176
177const SERVER_CONFIG_SAMPLE: &str = r#"log = "info"
179audit_file = "/var/log/manta/server-audit.log"
180
181[server]
182listen_address = "0.0.0.0"
183port = 8443
184cert = "/path/to/server.crt"
185key = "/path/to/server.key"
186console_inactivity_timeout_secs = 1800
187auth_rate_limit_per_minute = 60 # per source IP for /auth/*; omit to disable
188
189[sites.<site_name>]
190backend = "csm"
191shasta_base_url = "https://api.example.com"
192root_ca_cert_file = "/path/to/alps_root_cert.pem"
193"#;
194
195const SERVER_CONFIG_MIGRATION: &str = "\
197Migration from ~/.config/manta/config.toml:
198 copy these fields verbatim: log, audit_file, auditor, sites
199 add new [server] section: listen_address, port, cert, key,
200 console_inactivity_timeout_secs
201 drop (CLI-only): site, parent_hsm_group, hsm_group,
202 manta_server_url
203 drop (no longer recognised): sites.<X>.manta_server_url";
204
205fn missing_config_message(
206 binary: &str,
207 expected_path: &std::path::Path,
208 sample: &str,
209 migration: &str,
210) -> String {
211 let legacy_exists = get_default_manta_config_file_path()
212 .map(|p| p.exists())
213 .unwrap_or(false);
214 let mut msg = format!(
215 "{binary} configuration file '{}' not found.\n\nMinimal example:\n\n{sample}",
216 expected_path.to_string_lossy()
217 );
218 if legacy_exists {
219 msg.push('\n');
220 msg.push_str(migration);
221 }
222 msg
223}
224
225pub fn get_cli_configuration() -> Result<Config, Error> {
229 let path = get_cli_config_file_path()?;
230 if !path.exists() {
231 return Err(Error::NotFound(missing_config_message(
232 "CLI",
233 &path,
234 CLI_CONFIG_SAMPLE,
235 CLI_CONFIG_MIGRATION,
236 )));
237 }
238 let path_str = path.to_str().ok_or_else(|| {
239 Error::MissingField(
240 "CLI configuration file path contains invalid UTF-8".to_string(),
241 )
242 })?;
243 ::config::Config::builder()
244 .add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
245 .add_source(
246 ::config::Environment::with_prefix("MANTA")
247 .try_parsing(true)
248 .prefix_separator("_"),
249 )
250 .build()
251 .map_err(Error::ConfigError)
252}
253
254pub fn get_server_configuration() -> Result<Config, Error> {
258 let path = get_server_config_file_path()?;
259 if !path.exists() {
260 return Err(Error::NotFound(missing_config_message(
261 "Server",
262 &path,
263 SERVER_CONFIG_SAMPLE,
264 SERVER_CONFIG_MIGRATION,
265 )));
266 }
267 let path_str = path.to_str().ok_or_else(|| {
268 Error::MissingField(
269 "Server configuration file path contains invalid UTF-8".to_string(),
270 )
271 })?;
272 ::config::Config::builder()
273 .add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
274 .add_source(
275 ::config::Environment::with_prefix("MANTA")
276 .try_parsing(true)
277 .prefix_separator("_"),
278 )
279 .build()
280 .map_err(Error::ConfigError)
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use std::io::Write;
287 use std::sync::Mutex;
288 use tempfile::NamedTempFile;
289
290 static ENV_LOCK: Mutex<()> = Mutex::new(());
294
295 struct EnvGuard(&'static str);
299 impl EnvGuard {
300 fn set(key: &'static str, value: &str) -> Self {
301 unsafe { std::env::set_var(key, value) };
303 Self(key)
304 }
305 }
306 impl Drop for EnvGuard {
307 fn drop(&mut self) {
308 unsafe { std::env::remove_var(self.0) };
309 }
310 }
311
312 fn write_tmp_toml(content: &str) -> NamedTempFile {
313 let mut f = NamedTempFile::new().expect("tempfile");
314 f.write_all(content.as_bytes()).expect("write tempfile");
315 f
316 }
317
318 #[test]
319 fn default_cli_config_path_ends_with_cli_toml() {
320 let path = get_default_manta_cli_config_file_path().unwrap();
321 assert_eq!(path.file_name().unwrap(), "cli.toml");
322 }
323
324 #[test]
325 fn default_server_config_path_ends_with_server_toml() {
326 let path = get_default_manta_server_config_file_path().unwrap();
327 assert_eq!(path.file_name().unwrap(), "server.toml");
328 }
329
330 #[test]
331 fn default_legacy_config_path_ends_with_config_toml() {
332 let path = get_default_manta_config_file_path().unwrap();
333 assert_eq!(path.file_name().unwrap(), "config.toml");
334 }
335
336 #[test]
337 fn cli_and_server_default_paths_share_parent() {
338 let cli = get_default_manta_cli_config_file_path().unwrap();
339 let server = get_default_manta_server_config_file_path().unwrap();
340 assert_eq!(cli.parent(), server.parent());
341 }
342
343 #[test]
344 fn cli_config_file_path_honors_env_var() {
345 let _g = ENV_LOCK.lock().unwrap();
346 let _e = EnvGuard::set("MANTA_CLI_CONFIG", "/tmp/custom-cli.toml");
347 let path = get_cli_config_file_path().unwrap();
348 assert_eq!(path, PathBuf::from("/tmp/custom-cli.toml"));
349 }
350
351 #[test]
352 fn server_config_file_path_honors_env_var() {
353 let _g = ENV_LOCK.lock().unwrap();
354 let _e = EnvGuard::set("MANTA_SERVER_CONFIG", "/tmp/custom-server.toml");
355 let path = get_server_config_file_path().unwrap();
356 assert_eq!(path, PathBuf::from("/tmp/custom-server.toml"));
357 }
358
359 #[test]
360 fn cli_configuration_with_missing_file_returns_notfound() {
361 let _g = ENV_LOCK.lock().unwrap();
362 let _e = EnvGuard::set(
363 "MANTA_CLI_CONFIG",
364 "/nonexistent-dir/definitely-not-here.toml",
365 );
366 let err = get_cli_configuration().unwrap_err();
367 match err {
368 Error::NotFound(msg) => {
369 assert!(
370 msg.contains("CLI configuration file"),
371 "expected helpful NotFound message, got: {msg}"
372 );
373 assert!(
374 msg.contains("Minimal example"),
375 "expected sample TOML in message"
376 );
377 }
378 other => panic!("expected NotFound, got {other:?}"),
379 }
380 }
381
382 #[test]
383 fn cli_configuration_with_malformed_toml_returns_config_error() {
384 let _g = ENV_LOCK.lock().unwrap();
385 let bad = write_tmp_toml("this is = not [valid toml");
386 let _e = EnvGuard::set("MANTA_CLI_CONFIG", bad.path().to_str().unwrap());
387 let err = get_cli_configuration().unwrap_err();
388 assert!(
389 matches!(err, Error::ConfigError(_)),
390 "expected ConfigError variant, got {err:?}"
391 );
392 }
393
394 #[test]
395 fn cli_configuration_loads_valid_toml_and_env_var_overrides_file() {
396 let _g = ENV_LOCK.lock().unwrap();
397 let good = write_tmp_toml(
398 r#"log = "info"
399audit_file = "/tmp/x.log"
400site = "alps"
401parent_hsm_group = ""
402manta_server_url = "https://example:8443"
403"#,
404 );
405 let _path = EnvGuard::set("MANTA_CLI_CONFIG", good.path().to_str().unwrap());
406
407 let cfg = get_cli_configuration().unwrap();
408 assert_eq!(cfg.get_string("log").unwrap(), "info");
409 assert_eq!(cfg.get_string("site").unwrap(), "alps");
410 drop(cfg);
411
412 let _override = EnvGuard::set("MANTA_LOG", "trace");
415 let cfg = get_cli_configuration().unwrap();
416 assert_eq!(
417 cfg.get_string("log").unwrap(),
418 "trace",
419 "env var should override file value"
420 );
421 }
422
423 #[test]
424 fn server_configuration_with_missing_file_returns_notfound() {
425 let _g = ENV_LOCK.lock().unwrap();
426 let _e = EnvGuard::set(
427 "MANTA_SERVER_CONFIG",
428 "/nonexistent-dir/missing-server.toml",
429 );
430 let err = get_server_configuration().unwrap_err();
431 match err {
432 Error::NotFound(msg) => {
433 assert!(
434 msg.contains("Server configuration file"),
435 "expected helpful NotFound message, got: {msg}"
436 );
437 }
438 other => panic!("expected NotFound, got {other:?}"),
439 }
440 }
441
442 #[test]
443 fn server_configuration_loads_valid_toml() {
444 let _g = ENV_LOCK.lock().unwrap();
445 let good = write_tmp_toml(
446 r#"log = "info"
447audit_file = "/tmp/y.log"
448
449[server]
450listen_address = "0.0.0.0"
451port = 8443
452cert = "/etc/manta/cert.pem"
453key = "/etc/manta/key.pem"
454"#,
455 );
456 let _e =
457 EnvGuard::set("MANTA_SERVER_CONFIG", good.path().to_str().unwrap());
458 let cfg = get_server_configuration().unwrap();
459 assert_eq!(cfg.get_string("server.listen_address").unwrap(), "0.0.0.0");
460 assert_eq!(cfg.get_int("server.port").unwrap(), 8443);
461 }
462}