Skip to main content

mauth_client/
config.rs

1use crate::{CLIENT, MAuthInfo};
2use mauth_core::signer::Signer;
3use reqwest::Client;
4use reqwest::Url;
5use reqwest_middleware::ClientBuilder;
6use serde::Deserialize;
7use std::io;
8use thiserror::Error;
9use uuid::Uuid;
10
11const CONFIG_FILE: &str = ".mauth_config.yml";
12
13impl MAuthInfo {
14    /// Construct the MAuthInfo struct based on the contents of the config file `.mauth_config.yml`
15    /// present in the current user's home directory. Returns an enum error type that includes the
16    /// error types of all crates used.
17    pub fn from_default_file() -> Result<MAuthInfo, ConfigReadError> {
18        Self::from_config_section(&Self::config_section_from_default_file()?)
19    }
20
21    pub(crate) fn config_section_from_default_file() -> Result<ConfigFileSection, ConfigReadError> {
22        let mut home = dirs::home_dir().unwrap();
23        home.push(CONFIG_FILE);
24        let config_data = std::fs::read_to_string(&home)?;
25
26        let config_data_value: serde_yml::Value = serde_yml::from_slice(&config_data.into_bytes())?;
27        let common_section = config_data_value
28            .get("common")
29            .ok_or(ConfigReadError::InvalidFile(None))?;
30        let common_section_typed: ConfigFileSection =
31            serde_yml::from_value(common_section.clone())?;
32        Ok(common_section_typed)
33    }
34
35    /// Construct the MAuthInfo struct based on a passed-in ConfigFileSection instance. The
36    /// optional input_keystore is present to support internal cloning and need not be provided
37    /// if being used outside of the crate.
38    pub fn from_config_section(section: &ConfigFileSection) -> Result<MAuthInfo, ConfigReadError> {
39        let full_uri: Url = format!(
40            "{}/mauth/{}/security_tokens/",
41            &section.mauth_baseurl, &section.mauth_api_version
42        )
43        .parse()?;
44
45        let mut pk_data = section.private_key_data.clone();
46        if pk_data.is_none()
47            && let Some(pk_file_path) = section.private_key_file.as_ref()
48        {
49            pk_data = Some(std::fs::read_to_string(pk_file_path)?);
50        }
51        if pk_data.is_none() {
52            return Err(ConfigReadError::NoPrivateKey);
53        }
54
55        let mauth_info = MAuthInfo {
56            app_id: Uuid::parse_str(&section.app_uuid)?,
57            mauth_uri_base: full_uri,
58            sign_with_v1_also: !section.v2_only_sign_requests.unwrap_or(false),
59            allow_v1_auth: !section.v2_only_authenticate.unwrap_or(false),
60            signer: Signer::new(section.app_uuid.clone(), pk_data.unwrap())?,
61        };
62
63        CLIENT.get_or_init(|| {
64            let builder = ClientBuilder::new(Client::new()).with(mauth_info.clone());
65            #[cfg(any(
66                feature = "tracing-otel-26",
67                feature = "tracing-otel-27",
68                feature = "tracing-otel-28",
69                feature = "tracing-otel-29",
70                feature = "tracing-otel-30",
71                feature = "tracing-otel-31",
72            ))]
73            let builder = builder.with(reqwest_tracing::TracingMiddleware::default());
74            builder.build()
75        });
76
77        Ok(mauth_info)
78    }
79}
80
81/// All of the configuration data needed to set up a MAuthInfo struct. Implements Deserialize
82/// to be read from a YAML file easily, or can be created manually.
83#[derive(Deserialize, Clone)]
84pub struct ConfigFileSection {
85    pub app_uuid: String,
86    pub mauth_baseurl: String,
87    pub mauth_api_version: String,
88    pub private_key_file: Option<String>,
89    pub private_key_data: Option<String>,
90    pub v2_only_sign_requests: Option<bool>,
91    pub v2_only_authenticate: Option<bool>,
92}
93
94impl Default for ConfigFileSection {
95    fn default() -> Self {
96        Self {
97            app_uuid: "".to_string(),
98            mauth_baseurl: "".to_string(),
99            mauth_api_version: "v1".to_string(),
100            private_key_file: None,
101            private_key_data: None,
102            v2_only_sign_requests: Some(true),
103            v2_only_authenticate: Some(true),
104        }
105    }
106}
107
108/// All of the possible errors that can take place when attempting to read a config file. Errors
109/// are specific to the libraries that created them, and include the details from those libraries.
110#[derive(Debug, Error)]
111pub enum ConfigReadError {
112    #[error("File Read Error: {0}")]
113    FileReadError(#[from] io::Error),
114    #[error("Not a valid maudit config file: {0:?}")]
115    InvalidFile(Option<serde_yml::Error>),
116    #[error("MAudit URI not valid: {0}")]
117    InvalidUri(#[from] url::ParseError),
118    #[error("App UUID not valid: {0}")]
119    InvalidAppUuid(#[from] uuid::Error),
120    #[error("Unable to parse RSA private key: {0}")]
121    PrivateKeyDecodeError(String),
122    #[error("Neither private_key_file nor private_key_data were provided")]
123    NoPrivateKey,
124}
125
126impl From<mauth_core::error::Error> for ConfigReadError {
127    fn from(err: mauth_core::error::Error) -> ConfigReadError {
128        match err {
129            mauth_core::error::Error::PrivateKeyDecodeError(pkey_err) => {
130                ConfigReadError::PrivateKeyDecodeError(format!("{pkey_err}"))
131            }
132            _ => panic!("should not be possible to get this error type from signer construction"),
133        }
134    }
135}
136
137impl From<serde_yml::Error> for ConfigReadError {
138    fn from(err: serde_yml::Error) -> ConfigReadError {
139        ConfigReadError::InvalidFile(Some(err))
140    }
141}
142
143#[cfg(test)]
144mod test {
145    use super::*;
146    use tokio::fs;
147
148    #[tokio::test]
149    async fn invalid_uri_returns_right_error() {
150        let bad_config = ConfigFileSection {
151            app_uuid: "".to_string(),
152            mauth_baseurl: "dfaedfaewrfaew".to_string(),
153            mauth_api_version: "".to_string(),
154            private_key_file: Some("".to_string()),
155            private_key_data: None,
156            v2_only_sign_requests: None,
157            v2_only_authenticate: None,
158        };
159        let load_result = MAuthInfo::from_config_section(&bad_config);
160        assert!(matches!(load_result, Err(ConfigReadError::InvalidUri(_))));
161    }
162
163    #[tokio::test]
164    async fn bad_file_path_returns_right_error() {
165        let bad_config = ConfigFileSection {
166            app_uuid: "".to_string(),
167            mauth_baseurl: "https://example.com/".to_string(),
168            mauth_api_version: "v1".to_string(),
169            private_key_file: Some("no_such_file".to_string()),
170            private_key_data: None,
171            v2_only_sign_requests: None,
172            v2_only_authenticate: None,
173        };
174        let load_result = MAuthInfo::from_config_section(&bad_config);
175        assert!(matches!(
176            load_result,
177            Err(ConfigReadError::FileReadError(_))
178        ));
179    }
180
181    #[tokio::test]
182    async fn bad_key_file_returns_right_error() {
183        let filename = "dummy_file";
184        fs::write(&filename, b"definitely not a key").await.unwrap();
185        let bad_config = ConfigFileSection {
186            app_uuid: "c7db7fde-2448-11ef-b358-125eb8485a60".to_string(),
187            mauth_baseurl: "https://example.com/".to_string(),
188            mauth_api_version: "v1".to_string(),
189            private_key_file: Some(filename.to_string()),
190            private_key_data: None,
191            v2_only_sign_requests: None,
192            v2_only_authenticate: None,
193        };
194        let load_result = MAuthInfo::from_config_section(&bad_config);
195        fs::remove_file(&filename).await.unwrap();
196        assert!(matches!(
197            load_result,
198            Err(ConfigReadError::PrivateKeyDecodeError(_))
199        ));
200    }
201
202    #[tokio::test]
203    async fn bad_uuid_returns_right_error() {
204        let filename = "valid_key_file";
205        fs::write(&filename, "invalid data").await.unwrap();
206        let bad_config = ConfigFileSection {
207            app_uuid: "".to_string(),
208            mauth_baseurl: "https://example.com/".to_string(),
209            mauth_api_version: "v1".to_string(),
210            private_key_file: Some(filename.to_string()),
211            private_key_data: None,
212            v2_only_sign_requests: None,
213            v2_only_authenticate: None,
214        };
215        let load_result = MAuthInfo::from_config_section(&bad_config);
216        fs::remove_file(&filename).await.unwrap();
217        assert!(matches!(
218            load_result,
219            Err(ConfigReadError::InvalidAppUuid(_))
220        ));
221    }
222}