mfa_cli/
mfa.rs

1use super::config;
2use super::totp;
3use std::env;
4use std::fmt;
5use std::fs::{DirBuilder, File};
6use std::io::prelude::*;
7use std::path::Path;
8
9// 設定ファイルのルートディレクトリ
10const SAVE_DIR_NAME: &str = "mfa-cli";
11const HIDDEN_SAVE_DIR_NAME: &str = ".mfa-cli";
12// 設定ファイル名
13const CONFIG_FILE_NAME: &str = "profile";
14
15// for using print Profile
16#[derive(Debug)]
17pub struct Profile {
18    name: String,
19}
20
21impl fmt::Display for Profile {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "{}", self.name)
24    }
25}
26
27impl Profile {
28    pub fn new(name: String) -> Self {
29        Self { name }
30    }
31}
32
33#[derive(Debug, Default)]
34pub struct Mfa {
35    config: config::Config,
36    dump_file: DumpFile,
37}
38
39impl Mfa {
40    pub fn new() -> Result<Self, String> {
41        let mut this = Self {
42            config: Default::default(),
43            dump_file: Default::default(),
44        };
45
46        match this.setup() {
47            Ok(_) => Ok(this),
48            Err(err) => Err(err),
49        }
50    }
51
52    // Build new profile and register.
53    pub fn register_profile(&mut self, account_name: &str, secret: &str) -> Result<(), String> {
54        match self.config.new_profile(account_name, secret) {
55            Ok(_) => Ok(()),
56            Err(err) => Err(err.to_string()),
57        }
58    }
59
60    // Get all of profile list
61    pub fn list_profiles(&self) -> Vec<Profile> {
62        self.config
63            .get_profiles()
64            .iter()
65            .map(|profile| Profile::new(profile.get_name().to_string()))
66            .collect()
67    }
68
69    pub fn remove_profile(&mut self, profile_name: &str) -> Result<(), String> {
70        self.config.remove_profile(profile_name)
71    }
72
73    // Get the decoded secret value with a profile name.
74    pub fn get_secret_by_name(&self, profile_name: &str) -> Option<Vec<u8>> {
75        self.config.get_secret_by_name(profile_name)
76    }
77
78    // Get the authentication code with a profile name.
79    pub fn get_code_by_name(&self, profile_name: &str) -> Result<String, String> {
80        match self.get_secret_by_name(profile_name) {
81            Some(secret) => totp::totp(secret.as_ref()),
82            None => Err(format!(
83                "can't get the secret that profile: {}",
84                profile_name
85            )),
86        }
87    }
88
89    // Dump config to file
90    pub fn dump(&self) -> Result<(), String> {
91        let config_data = match self.config.serialize() {
92            Ok(data) => data,
93            Err(err) => return Err(err),
94        };
95
96        let mut file = match File::create(self.dump_file.path()) {
97            Ok(file) => file,
98            Err(err) => return Err(err.to_string()),
99        };
100        match file.write_all(config_data.as_bytes()) {
101            Ok(()) => Ok(()),
102            Err(err) => Err(err.to_string()),
103        }
104    }
105
106    // Restore config from file
107    pub fn restore(&mut self) -> Result<(), String> {
108        let mut file = match File::open(self.dump_file.path()) {
109            Ok(file) => file,
110            Err(err) => return Err(err.to_string()),
111        };
112        let mut contents = String::new();
113        if let Err(err) = file.read_to_string(&mut contents) {
114            return Err(err.to_string());
115        };
116
117        self.config.deserialize(&contents)
118    }
119
120    // Run setup steps.
121    //
122    // Restore config if a dump file exists already.
123    // Otherwise do nothing.
124    fn setup(&mut self) -> Result<(), String> {
125        // Create save dir
126        if !self.dump_file.dir_exists() {
127            if let Err(err) = DirBuilder::new()
128                .recursive(true)
129                .create(self.dump_file.dir_path())
130            {
131                return Err(err.to_string());
132            }
133        }
134
135        // nothing to do if it does not exist
136        if !self.dump_file.exists() {
137            return Ok(());
138        }
139
140        if self.dump_file.check() {
141            return self.restore();
142        }
143
144        Ok(())
145    }
146}
147
148#[derive(Debug)]
149struct DumpFile {
150    dir: Box<Path>,
151    file_name: &'static str,
152}
153
154impl Default for DumpFile {
155    fn default() -> Self {
156        let path = fetch_dump_path().to_path_buf();
157
158        Self {
159            dir: path.into_boxed_path(),
160            file_name: CONFIG_FILE_NAME,
161        }
162    }
163}
164
165impl DumpFile {
166    // It returns true if dump file exists.
167    fn exists(&self) -> bool {
168        self.path().exists()
169    }
170
171    fn dir_exists(&self) -> bool {
172        self.dir_path().exists()
173    }
174
175    // Check the dump file state.
176    // It returns true if the dump file is restorable condition.
177    //
178    // Conditions is the file exists, it is file and file size is not empty.
179    fn check(&self) -> bool {
180        if !self.exists() {
181            return false;
182        }
183
184        let meta = match self.path().metadata() {
185            Ok(meta) => meta,
186            Err(_) => return false,
187        };
188
189        meta.is_file() && 0 < meta.len()
190    }
191
192    fn path(&self) -> Box<Path> {
193        let mut path = self.dir.to_path_buf();
194        path.push(self.file_name);
195        path.into_boxed_path()
196    }
197
198    fn dir_path(&self) -> &Path {
199        &self.dir
200    }
201}
202
203// decides directory which dump config file
204fn fetch_dump_path() -> Box<Path> {
205    if let Some(path) = env_my_home() {
206        let mut path = Path::new(&path).to_path_buf();
207        path.push(SAVE_DIR_NAME);
208        return path.into_boxed_path();
209    }
210
211    if let Some(path) = env_xdg_config_home() {
212        let mut path = Path::new(&path).to_path_buf();
213        path.push(SAVE_DIR_NAME);
214        return path.into_boxed_path();
215    }
216
217    if let Some(path) = env_home() {
218        let mut path = Path::new(&path).to_path_buf();
219        path.push(HIDDEN_SAVE_DIR_NAME);
220        return path.into_boxed_path();
221    }
222
223    if let Ok(mut path) = env::current_dir() {
224        path.push(HIDDEN_SAVE_DIR_NAME);
225        return path.into_boxed_path();
226    }
227
228    panic!("can't find save directory");
229}
230
231fn env_my_home() -> Option<String> {
232    match env::var("MFA_CLI_CONFIG_HOME") {
233        Ok(path) if Path::new(&path).exists() => Some(path),
234        Ok(path) if !Path::new(&path).exists() => {
235            DirBuilder::new().recursive(true).create(&path).unwrap();
236            Some(path)
237        }
238        _ => None,
239    }
240}
241
242fn env_xdg_config_home() -> Option<String> {
243    match env::var("XDG_CONFIG_HOME") {
244        Ok(path) if Path::new(&path).exists() => Some(path),
245        _ => None,
246    }
247}
248
249fn env_home() -> Option<String> {
250    match env::var("HOME") {
251        Ok(path) if Path::new(&path).exists() => Some(path),
252        _ => None,
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use tempfile;
260
261    #[test]
262    fn dump_file_path() {
263        let dump_file = DumpFile {
264            dir: Path::new("/path/to").to_path_buf().into_boxed_path(),
265            file_name: "file",
266        };
267
268        let path = dump_file.path();
269
270        assert_eq!(*path.as_ref(), *Path::new("/path/to/file"));
271    }
272
273    #[test]
274    #[ignore]
275    fn setup_config_dir_for_for_xdg_config_home() {
276        let config_home = tempfile::tempdir().unwrap();
277        env::set_var("MFA_CLI_CONFIG_HOME", config_home.path().to_str().unwrap());
278
279        let _ = Mfa::new();
280        assert!(config_home.path().join("mfa-cli").is_dir());
281    }
282
283    #[test]
284    #[ignore]
285    fn setup_config_dir_for_current_dir() {
286        let pwd = env::current_dir().unwrap();
287        let config_home = tempfile::tempdir().unwrap();
288
289        env::remove_var("MFA_CLI_CONFIG_HOME");
290        env::remove_var("XDG_CONFIG_HOME");
291        env::remove_var("HOME");
292        let _ = env::set_current_dir(config_home.path());
293
294        let _ = Mfa::new();
295        assert!(config_home.path().join(".mfa-cli").is_dir());
296
297        let _ = env::set_current_dir(pwd);
298    }
299
300    #[test]
301    fn fetch_dump_path_from_env_my_home_when_that_exists() {
302        let current_dir = env::current_dir().unwrap();
303        let expected = current_dir.join("tests/tmp/mfa-cli");
304        env::set_var("MFA_CLI_CONFIG_HOME", current_dir.join("tests/tmp"));
305
306        assert_eq!(*fetch_dump_path(), *expected);
307    }
308
309    #[test]
310    fn fetch_dump_path_from_env_my_home_when_that_does_not_exist() {
311        let current_dir = env::current_dir().unwrap();
312
313        let expected = current_dir.join("tests/tmp/does_not_exist/mfa-cli");
314        let config_home_path = current_dir.join("tests/tmp/does_not_exist");
315        env::set_var("MFA_CLI_CONFIG_HOME", config_home_path.clone());
316
317        assert_eq!(*fetch_dump_path(), *expected);
318        std::fs::remove_dir(config_home_path).unwrap();
319    }
320
321    // NOTE: The reason this test is marked as ignore is that environment variables conflict between tests
322    //       You have to append single thread option when run tests
323    #[test]
324    #[ignore]
325    fn fetch_dump_path_from_env_xdg_config_home() {
326        env::remove_var("MFA_CLI_CONFIG_HOME");
327        env::set_var("XDG_CONFIG_HOME", "./tests/tmp");
328        assert_eq!(*fetch_dump_path(), *Path::new("./tests/tmp/mfa-cli"));
329    }
330
331    #[test]
332    fn fetch_dump_path_from_env_home() {
333        env::remove_var("MFA_CLI_CONFIG_HOME");
334        env::remove_var("XDG_CONFIG_HOME");
335
336        env::set_var("HOME", "./tests/tmp");
337        assert_eq!(*fetch_dump_path(), *Path::new("./tests/tmp/.mfa-cli"));
338    }
339
340    // NOTE: The reason this test is marked as ignore is that environment variables conflict between tests
341    //       You have to append single thread option when run tests
342    #[test]
343    #[ignore]
344    fn fetch_dump_path_from_current_dir() {
345        env::remove_var("MFA_CLI_CONFIG_HOME");
346        env::remove_var("XDG_CONFIG_HOME");
347        env::remove_var("HOME");
348
349        let expected = env::current_dir().unwrap().join(".mfa-cli");
350        assert_eq!(*fetch_dump_path(), expected);
351    }
352
353    #[test]
354    fn setup_when_there_is_empty_config_file() {
355        env::set_var("MFA_CLI_CONFIG_HOME", "tests/tmp/");
356        std::fs::create_dir_all("tests/tmp/mfa-cli/").unwrap();
357        File::create("tests/tmp/mfa-cli/profile").unwrap();
358
359        assert!(Mfa::new().is_ok());
360
361        std::fs::remove_file("tests/tmp/mfa-cli/profile").unwrap()
362    }
363
364    #[test]
365    fn test_remove_profile() {
366        let mut mfa: Mfa = Default::default();
367        mfa.config.new_profile("test", "hoge").unwrap();
368
369        mfa.remove_profile("test").unwrap();
370        assert!(mfa.get_secret_by_name("test").is_none());
371    }
372
373    #[test]
374    fn test_list_profiles() {
375        let mut mfa: Mfa = Default::default();
376        mfa.config.new_profile("test1", "hoge").unwrap();
377        mfa.config.new_profile("test2", "hoge").unwrap();
378
379        let profiles = mfa.list_profiles();
380        assert_eq!(profiles.get(0).unwrap().name, "test1");
381        assert_eq!(profiles.get(1).unwrap().name, "test2");
382        assert!(profiles.get(2).is_none());
383    }
384}