modality_reflector_config/
resolve.rs

1use std::{
2    env,
3    path::{Path, PathBuf},
4};
5
6use modality_auth_token::{
7    decode_auth_token_hex, token_user_file::REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME, AuthToken,
8};
9
10use crate::{try_from_file, Config, ConfigLoadError, CONFIG_ENV_VAR};
11
12const CONFIG_FILE_NAME: &str = "config.toml";
13const CONFIG_DIR: &str = "modality-reflector";
14const SYS_CONFIG_BASE_PATH: &str = "/etc";
15
16pub fn load_config_and_auth_token(
17    manually_provided_config_path: Option<PathBuf>,
18    manually_provided_auth_token: Option<PathBuf>,
19) -> Result<(crate::refined::Config, AuthToken), Box<dyn std::error::Error + Send + Sync>> {
20    let ConfigContext {
21        config: cfg,
22        config_file_parent_dir,
23        ..
24    } = ConfigContext::load_default(manually_provided_config_path)?;
25
26    let auth_token =
27        resolve_reflector_auth_token(manually_provided_auth_token, &config_file_parent_dir)?;
28    Ok((cfg, auth_token))
29}
30
31/// Attempt to load a `config.toml` configuration file from the following locations:
32/// - system configuration directory (i.e. /etc/modality-reflector/config.toml on Linux)
33/// - `dirs::config_dir()` (i.e. ~/.config/modality-reflector/config.toml on Linux)
34/// - Environment variable `MODALITY_REFLECTOR_CONFIG`
35/// - Manually provided path (i.e. at the CLI with `--config file`)
36///
37/// The files are read in the order given above, with last file found
38/// taking precedence over files read earlier.
39///
40/// If a configuration file doesn't exists in any of the locations, None is returned.
41pub fn load_config(
42    manually_provided_config_path: Option<PathBuf>,
43) -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
44    let mut cfg = load_system_config()?;
45    if let Some(other_cfg) = load_user_config()? {
46        cfg.replace(other_cfg);
47    }
48    if let Some(other_cfg) = load_env_config()? {
49        cfg.replace(other_cfg);
50    }
51    if let Some(other_cfg_path) = manually_provided_config_path {
52        if let Some(config_file_parent_dir) = other_cfg_path.parent().map(ToOwned::to_owned) {
53            let other_cfg = ConfigContext {
54                config: try_from_file(other_cfg_path.as_path())?,
55                config_file: Some(other_cfg_path),
56                config_file_parent_dir,
57            };
58            cfg.replace(other_cfg);
59        }
60    }
61    Ok(cfg)
62}
63
64pub struct ConfigContext {
65    pub config: Config,
66    pub config_file: Option<PathBuf>,
67    pub config_file_parent_dir: PathBuf,
68}
69
70impl ConfigContext {
71    pub fn load_default(
72        config_file_override: Option<PathBuf>,
73    ) -> Result<Self, ExpandedConfigLoadError> {
74        if let Some(cc) = load_config(config_file_override)? {
75            Ok(cc)
76        } else {
77            let config_file_parent_dir = env::current_dir().map_err(|ioerr| {
78                ExpandedConfigLoadError::ConfigLoadError(ConfigLoadError::Io(ioerr))
79            })?;
80            Ok(ConfigContext {
81                config: Default::default(),
82                config_file: None,
83                config_file_parent_dir,
84            })
85        }
86    }
87}
88
89fn load_system_config() -> Result<Option<ConfigContext>, ConfigLoadError> {
90    let cfg_path = system_config_path();
91    if cfg_path.exists() {
92        tracing::trace!("Load system configuration file {}", cfg_path.display());
93        if let Some(config_file_parent_dir) = cfg_path.parent().map(ToOwned::to_owned) {
94            Ok(Some(ConfigContext {
95                config: try_from_file(&cfg_path)?,
96                config_file: Some(cfg_path),
97                config_file_parent_dir,
98            }))
99        } else {
100            Ok(None)
101        }
102    } else {
103        Ok(None)
104    }
105}
106
107fn load_user_config() -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
108    load_user_or_env_config(UserOrEnvPath::User)
109}
110
111fn load_env_config() -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
112    load_user_or_env_config(UserOrEnvPath::Env)
113}
114
115fn load_user_or_env_config(
116    loc: UserOrEnvPath,
117) -> Result<Option<ConfigContext>, ExpandedConfigLoadError> {
118    let cfg_path = match loc {
119        UserOrEnvPath::User => user_config_path(),
120        UserOrEnvPath::Env => env_config_path()?,
121    };
122    match cfg_path {
123        Some(p) if p.exists() => {
124            tracing::trace!("Load {} configuration file {}", loc, p.display());
125            if let Some(config_file_parent_dir) = p.as_path().parent().map(ToOwned::to_owned) {
126                Ok(Some(ConfigContext {
127                    config: try_from_file(&p)?,
128                    config_file: Some(p),
129                    config_file_parent_dir,
130                }))
131            } else {
132                Ok(None)
133            }
134        }
135        _ => Ok(None),
136    }
137}
138
139fn system_config_path() -> PathBuf {
140    PathBuf::from(SYS_CONFIG_BASE_PATH)
141        .join(CONFIG_DIR)
142        .join(CONFIG_FILE_NAME)
143}
144
145fn user_config_path() -> Option<PathBuf> {
146    dirs::config_dir().map(|d| d.join(CONFIG_DIR).join(CONFIG_FILE_NAME))
147}
148
149fn env_config_path() -> Result<Option<PathBuf>, ExpandedConfigLoadError> {
150    match env::var(CONFIG_ENV_VAR) {
151        Ok(env_path) => Ok(PathBuf::from(env_path).into()),
152        Err(env::VarError::NotPresent) => Ok(None),
153        Err(env::VarError::NotUnicode(_)) => {
154            Err(ExpandedConfigLoadError::EnvVarSpecifiedConfigNonUtf8)
155        }
156    }
157}
158
159#[derive(Debug, thiserror::Error)]
160pub enum ExpandedConfigLoadError {
161    #[error(
162        "The {} environment variable contained a non-UTF-8-compatible path.",
163        CONFIG_ENV_VAR
164    )]
165    EnvVarSpecifiedConfigNonUtf8,
166    #[error("Config loading error.")]
167    ConfigLoadError(
168        #[source]
169        #[from]
170        ConfigLoadError,
171    ),
172}
173
174#[derive(Copy, Clone, Debug)]
175enum UserOrEnvPath {
176    User,
177    Env,
178}
179
180impl std::fmt::Display for UserOrEnvPath {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        match self {
183            UserOrEnvPath::User => f.write_str("user"),
184            UserOrEnvPath::Env => f.write_str("environment"),
185        }
186    }
187}
188
189/// * CLI path override
190/// * Env-Var MODALITY_AUTH_TOKEN
191/// * file relative to process current working directory
192/// * file relative to config file parent directory
193pub fn resolve_reflector_auth_token(
194    cli_override_auth_token_file_path: Option<PathBuf>,
195    config_file_parent_directory: &Path,
196) -> Result<AuthToken, Box<dyn std::error::Error + Send + Sync>> {
197    if let Some(path) = cli_override_auth_token_file_path {
198        if !path.exists() {
199            return Err(ReflectorAuthTokenLoadError::CliProvidedAuthTokenFileDidNotExist.into());
200        }
201        if let Some(token_file_contents) =
202            modality_auth_token::token_user_file::read_user_auth_token_file(&path)?
203        {
204            return Ok(token_file_contents.auth_token);
205        }
206    }
207
208    match env::var("MODALITY_AUTH_TOKEN") {
209        Ok(val) => {
210            tracing::trace!("Loading CLI env context auth token");
211            return Ok(decode_auth_token_hex(&val)?);
212        }
213        Err(env::VarError::NotUnicode(_)) => {
214            return Err(
215                ReflectorAuthTokenLoadError::EnvVarSpecifiedModalityAuthTokenNonUtf8.into(),
216            );
217        }
218        Err(env::VarError::NotPresent) => {
219            // Fall through and try the next approach
220        }
221    }
222    if let Ok(cwd) = std::env::current_dir() {
223        let cwd_relative_path = cwd.join(REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME);
224        if cwd_relative_path.exists() {
225            if let Some(token_file_contents) =
226                modality_auth_token::token_user_file::read_user_auth_token_file(&cwd_relative_path)?
227            {
228                return Ok(token_file_contents.auth_token);
229            }
230        }
231    }
232
233    let config_relative_path =
234        config_file_parent_directory.join(REFLECTOR_AUTH_TOKEN_DEFAULT_FILE_NAME);
235
236    if let Some(token_file_contents) =
237        modality_auth_token::token_user_file::read_user_auth_token_file(&config_relative_path)?
238    {
239        return Ok(token_file_contents.auth_token);
240    }
241
242    // read the modality cli auth token as a fallback
243    if let Ok(auth_token) = AuthToken::load() {
244        return Ok(auth_token);
245    }
246
247    Err(ReflectorAuthTokenLoadError::Underspecified.into())
248}
249
250#[derive(Debug, thiserror::Error)]
251pub enum ReflectorAuthTokenLoadError {
252    #[error("CLI provided auth token file did not exist")]
253    CliProvidedAuthTokenFileDidNotExist,
254
255    #[error(
256        "The MODALITY_AUTH_TOKEN environment variable contained a non-UTF-8-compatible string"
257    )]
258    EnvVarSpecifiedModalityAuthTokenNonUtf8,
259
260    #[error("No auth token was specified.  Provide a path to a token file as a CLI argument or put the token hex contents into the MODALITY_AUTH_TOKEN environment path")]
261    Underspecified,
262}