Skip to main content

manta_shared/common/config/
mod.rs

1pub 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
14/// Returns the XDG-compliant `ProjectDirs` for manta.
15///
16/// All path helpers in this module delegate to this function
17/// so the qualifier/organization/application triple is defined
18/// in exactly one place.
19fn get_project_dirs() -> Result<ProjectDirs, Error> {
20  ProjectDirs::from(
21    "local", /*qualifier*/
22    "cscs",  /*organization*/
23    "manta", /*application*/
24  )
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
34/// Returns the default manta config directory path
35/// (e.g. `~/.config/manta/`).
36pub fn get_default_config_path() -> Result<PathBuf, Error> {
37  Ok(PathBuf::from(get_project_dirs()?.config_dir()))
38}
39
40/// Returns the path of the *legacy* unified config file
41/// (e.g. `~/.config/manta/config.toml`). Used only by
42/// `missing_config_message` to detect when a user is migrating from
43/// the pre-split layout — neither binary ever reads from this path
44/// at startup.
45pub 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
51/// Returns the default CLI config file path
52/// (e.g. `~/.config/manta/cli.toml`).
53pub 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
59/// Returns the default server config file path
60/// (e.g. `~/.config/manta/server.toml`).
61pub 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
67/// Returns the default manta cache directory path
68/// (e.g. `~/.cache/manta/`).
69pub fn get_default_cache_path() -> Result<PathBuf, Error> {
70  Ok(PathBuf::from(get_project_dirs()?.cache_dir()))
71}
72
73/// Reads the manta CLI configuration file (`cli.toml`) and parses it as
74/// TOML, honoring `MANTA_CLI_CONFIG`.
75///
76/// Returns both the file path (for later writing) and the
77/// parsed `DocumentMut`.
78pub 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
93/// Writes a `DocumentMut` back to the manta configuration file.
94pub 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
109/// Read the root CA certificate from `file_path`, falling
110/// back to the default config directory if the path is
111/// relative.
112pub 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
135/// Returns the CLI config file path, honoring `MANTA_CLI_CONFIG` if set.
136pub 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
144/// Returns the server config file path, honoring `MANTA_SERVER_CONFIG` if set.
145pub 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
153/// Minimal CLI config sample shown in the NotFound error.
154const 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
166/// Migration mapping shown when a legacy `config.toml` is detected.
167const 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
177/// Minimal server config sample shown in the NotFound error.
178const 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
195/// Migration mapping shown when a legacy `config.toml` is detected.
196const 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
225/// Load `cli.toml`. Fails loudly if the file is missing; the error
226/// message includes a minimal example and (when a legacy config.toml is
227/// detected) a field-by-field migration mapping.
228pub 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
254/// Load `server.toml`. Fails loudly if the file is missing; the error
255/// message includes a minimal example and (when a legacy config.toml is
256/// detected) a field-by-field migration mapping.
257pub 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
287  #[test]
288  fn default_cli_config_path_ends_with_cli_toml() {
289    let path = get_default_manta_cli_config_file_path().unwrap();
290    assert_eq!(path.file_name().unwrap(), "cli.toml");
291  }
292
293  #[test]
294  fn default_server_config_path_ends_with_server_toml() {
295    let path = get_default_manta_server_config_file_path().unwrap();
296    assert_eq!(path.file_name().unwrap(), "server.toml");
297  }
298
299  #[test]
300  fn default_legacy_config_path_ends_with_config_toml() {
301    let path = get_default_manta_config_file_path().unwrap();
302    assert_eq!(path.file_name().unwrap(), "config.toml");
303  }
304
305  #[test]
306  fn cli_and_server_default_paths_share_parent() {
307    let cli = get_default_manta_cli_config_file_path().unwrap();
308    let server = get_default_manta_server_config_file_path().unwrap();
309    assert_eq!(cli.parent(), server.parent());
310  }
311}