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