pub mod types;
use std::{
fs::{self, File},
io::{Read, Write},
path::PathBuf,
};
use crate::common::error::MantaError as Error;
use config::Config;
use directories::ProjectDirs;
use toml_edit::DocumentMut;
fn get_project_dirs() -> Result<ProjectDirs, Error> {
ProjectDirs::from(
"local",
"cscs",
"manta",
)
.ok_or_else(|| {
Error::MissingField(
"Could not determine project directories \
(home directory may not be set)"
.to_string(),
)
})
}
pub fn get_default_config_path() -> Result<PathBuf, Error> {
Ok(PathBuf::from(get_project_dirs()?.config_dir()))
}
pub fn get_default_manta_config_file_path() -> Result<PathBuf, Error> {
let mut path = get_default_config_path()?;
path.push("config.toml");
Ok(path)
}
pub fn get_default_manta_cli_config_file_path() -> Result<PathBuf, Error> {
let mut path = get_default_config_path()?;
path.push("cli.toml");
Ok(path)
}
pub fn get_default_manta_server_config_file_path() -> Result<PathBuf, Error> {
let mut path = get_default_config_path()?;
path.push("server.toml");
Ok(path)
}
pub fn get_default_cache_path() -> Result<PathBuf, Error> {
Ok(PathBuf::from(get_project_dirs()?.cache_dir()))
}
pub fn read_config_toml() -> Result<(PathBuf, DocumentMut), Error> {
let path = get_cli_config_file_path()?;
tracing::debug!(
"Reading manta CLI configuration from {}",
path.to_string_lossy()
);
let content = fs::read_to_string(&path)?;
let doc = content.parse::<DocumentMut>()?;
Ok((path, doc))
}
pub fn write_config_toml(
path: &std::path::Path,
doc: &DocumentMut,
) -> Result<(), Error> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(path)?;
file.write_all(doc.to_string().as_bytes())?;
file.flush()?;
Ok(())
}
pub fn get_csm_root_cert_content(file_path: &str) -> Result<Vec<u8>, Error> {
let mut buf = Vec::new();
let root_cert_file_rslt = File::open(file_path);
let file_rslt = if root_cert_file_rslt.is_err() {
let mut config_path = get_default_config_path()?;
config_path.push(file_path);
File::open(config_path)
} else {
root_cert_file_rslt
};
match file_rslt {
Ok(mut file) => {
file.read_to_end(&mut buf)?;
Ok(buf)
}
Err(_) => Err(Error::NotFound(
"CA public root file could not be found".to_string(),
)),
}
}
pub fn get_cli_config_file_path() -> Result<PathBuf, Error> {
if let Ok(env_path) = std::env::var("MANTA_CLI_CONFIG") {
Ok(PathBuf::from(env_path))
} else {
get_default_manta_cli_config_file_path()
}
}
pub fn get_server_config_file_path() -> Result<PathBuf, Error> {
if let Ok(env_path) = std::env::var("MANTA_SERVER_CONFIG") {
Ok(PathBuf::from(env_path))
} else {
get_default_manta_server_config_file_path()
}
}
const CLI_CONFIG_SAMPLE: &str = r#"log = "info"
audit_file = "/tmp/manta-cli-audit.log"
site = "<site_name>"
parent_hsm_group = ""
manta_server_url = "https://manta-server.example.com:8443"
[sites.<site_name>]
backend = "csm" # or "ochami"
shasta_base_url = "https://api.example.com"
root_ca_cert_file = "alps_root_cert.pem"
"#;
const CLI_CONFIG_MIGRATION: &str = "\
Migration from ~/.config/manta/config.toml:
copy these fields verbatim: log, audit_file, site, parent_hsm_group,
auditor, sites
add CLI-only (now required): manta_server_url = \"https://...\"
(CLI talks only to the manta server)
drop (no longer recognised): sites.<X>.manta_server_url
do not copy (server-only fields): the [server] section belongs in
server.toml, not cli.toml";
const SERVER_CONFIG_SAMPLE: &str = r#"log = "info"
audit_file = "/var/log/manta/server-audit.log"
[server]
listen_address = "0.0.0.0"
port = 8443
cert = "/path/to/server.crt"
key = "/path/to/server.key"
console_inactivity_timeout_secs = 1800
auth_rate_limit_per_minute = 60 # per source IP for /auth/*; omit to disable
[sites.<site_name>]
backend = "csm"
shasta_base_url = "https://api.example.com"
root_ca_cert_file = "/path/to/alps_root_cert.pem"
"#;
const SERVER_CONFIG_MIGRATION: &str = "\
Migration from ~/.config/manta/config.toml:
copy these fields verbatim: log, audit_file, auditor, sites
add new [server] section: listen_address, port, cert, key,
console_inactivity_timeout_secs
drop (CLI-only): site, parent_hsm_group, hsm_group,
manta_server_url
drop (no longer recognised): sites.<X>.manta_server_url";
fn missing_config_message(
binary: &str,
expected_path: &std::path::Path,
sample: &str,
migration: &str,
) -> String {
let legacy_exists = get_default_manta_config_file_path()
.map(|p| p.exists())
.unwrap_or(false);
let mut msg = format!(
"{binary} configuration file '{}' not found.\n\nMinimal example:\n\n{sample}",
expected_path.to_string_lossy()
);
if legacy_exists {
msg.push('\n');
msg.push_str(migration);
}
msg
}
pub fn get_cli_configuration() -> Result<Config, Error> {
let path = get_cli_config_file_path()?;
if !path.exists() {
return Err(Error::NotFound(missing_config_message(
"CLI",
&path,
CLI_CONFIG_SAMPLE,
CLI_CONFIG_MIGRATION,
)));
}
let path_str = path.to_str().ok_or_else(|| {
Error::MissingField(
"CLI configuration file path contains invalid UTF-8".to_string(),
)
})?;
::config::Config::builder()
.add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
.add_source(
::config::Environment::with_prefix("MANTA")
.try_parsing(true)
.prefix_separator("_"),
)
.build()
.map_err(Error::ConfigError)
}
pub fn get_server_configuration() -> Result<Config, Error> {
let path = get_server_config_file_path()?;
if !path.exists() {
return Err(Error::NotFound(missing_config_message(
"Server",
&path,
SERVER_CONFIG_SAMPLE,
SERVER_CONFIG_MIGRATION,
)));
}
let path_str = path.to_str().ok_or_else(|| {
Error::MissingField(
"Server configuration file path contains invalid UTF-8".to_string(),
)
})?;
::config::Config::builder()
.add_source(::config::File::new(path_str, ::config::FileFormat::Toml))
.add_source(
::config::Environment::with_prefix("MANTA")
.try_parsing(true)
.prefix_separator("_"),
)
.build()
.map_err(Error::ConfigError)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::sync::Mutex;
use tempfile::NamedTempFile;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard(&'static str);
impl EnvGuard {
fn set(key: &'static str, value: &str) -> Self {
unsafe { std::env::set_var(key, value) };
Self(key)
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe { std::env::remove_var(self.0) };
}
}
fn write_tmp_toml(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::new().expect("tempfile");
f.write_all(content.as_bytes()).expect("write tempfile");
f
}
#[test]
fn default_cli_config_path_ends_with_cli_toml() {
let path = get_default_manta_cli_config_file_path().unwrap();
assert_eq!(path.file_name().unwrap(), "cli.toml");
}
#[test]
fn default_server_config_path_ends_with_server_toml() {
let path = get_default_manta_server_config_file_path().unwrap();
assert_eq!(path.file_name().unwrap(), "server.toml");
}
#[test]
fn default_legacy_config_path_ends_with_config_toml() {
let path = get_default_manta_config_file_path().unwrap();
assert_eq!(path.file_name().unwrap(), "config.toml");
}
#[test]
fn cli_and_server_default_paths_share_parent() {
let cli = get_default_manta_cli_config_file_path().unwrap();
let server = get_default_manta_server_config_file_path().unwrap();
assert_eq!(cli.parent(), server.parent());
}
#[test]
fn cli_config_file_path_honors_env_var() {
let _g = ENV_LOCK.lock().unwrap();
let _e = EnvGuard::set("MANTA_CLI_CONFIG", "/tmp/custom-cli.toml");
let path = get_cli_config_file_path().unwrap();
assert_eq!(path, PathBuf::from("/tmp/custom-cli.toml"));
}
#[test]
fn server_config_file_path_honors_env_var() {
let _g = ENV_LOCK.lock().unwrap();
let _e = EnvGuard::set("MANTA_SERVER_CONFIG", "/tmp/custom-server.toml");
let path = get_server_config_file_path().unwrap();
assert_eq!(path, PathBuf::from("/tmp/custom-server.toml"));
}
#[test]
fn cli_configuration_with_missing_file_returns_notfound() {
let _g = ENV_LOCK.lock().unwrap();
let _e = EnvGuard::set(
"MANTA_CLI_CONFIG",
"/nonexistent-dir/definitely-not-here.toml",
);
let err = get_cli_configuration().unwrap_err();
match err {
Error::NotFound(msg) => {
assert!(
msg.contains("CLI configuration file"),
"expected helpful NotFound message, got: {msg}"
);
assert!(
msg.contains("Minimal example"),
"expected sample TOML in message"
);
}
other => panic!("expected NotFound, got {other:?}"),
}
}
#[test]
fn cli_configuration_with_malformed_toml_returns_config_error() {
let _g = ENV_LOCK.lock().unwrap();
let bad = write_tmp_toml("this is = not [valid toml");
let _e = EnvGuard::set("MANTA_CLI_CONFIG", bad.path().to_str().unwrap());
let err = get_cli_configuration().unwrap_err();
assert!(
matches!(err, Error::ConfigError(_)),
"expected ConfigError variant, got {err:?}"
);
}
#[test]
fn cli_configuration_loads_valid_toml_and_env_var_overrides_file() {
let _g = ENV_LOCK.lock().unwrap();
let good = write_tmp_toml(
r#"log = "info"
audit_file = "/tmp/x.log"
site = "alps"
parent_hsm_group = ""
manta_server_url = "https://example:8443"
"#,
);
let _path = EnvGuard::set("MANTA_CLI_CONFIG", good.path().to_str().unwrap());
let cfg = get_cli_configuration().unwrap();
assert_eq!(cfg.get_string("log").unwrap(), "info");
assert_eq!(cfg.get_string("site").unwrap(), "alps");
drop(cfg);
let _override = EnvGuard::set("MANTA_LOG", "trace");
let cfg = get_cli_configuration().unwrap();
assert_eq!(
cfg.get_string("log").unwrap(),
"trace",
"env var should override file value"
);
}
#[test]
fn server_configuration_with_missing_file_returns_notfound() {
let _g = ENV_LOCK.lock().unwrap();
let _e = EnvGuard::set(
"MANTA_SERVER_CONFIG",
"/nonexistent-dir/missing-server.toml",
);
let err = get_server_configuration().unwrap_err();
match err {
Error::NotFound(msg) => {
assert!(
msg.contains("Server configuration file"),
"expected helpful NotFound message, got: {msg}"
);
}
other => panic!("expected NotFound, got {other:?}"),
}
}
#[test]
fn server_configuration_loads_valid_toml() {
let _g = ENV_LOCK.lock().unwrap();
let good = write_tmp_toml(
r#"log = "info"
audit_file = "/tmp/y.log"
[server]
listen_address = "0.0.0.0"
port = 8443
cert = "/etc/manta/cert.pem"
key = "/etc/manta/key.pem"
"#,
);
let _e =
EnvGuard::set("MANTA_SERVER_CONFIG", good.path().to_str().unwrap());
let cfg = get_server_configuration().unwrap();
assert_eq!(cfg.get_string("server.listen_address").unwrap(), "0.0.0.0");
assert_eq!(cfg.get_int("server.port").unwrap(), 8443);
}
}