mfa_cli/
config.rs

1extern crate base32;
2extern crate regex;
3extern crate serde;
4extern crate toml;
5
6use regex::Regex;
7use serde::Deserialize;
8use serde::Serialize;
9use std::fmt;
10
11#[derive(Debug, PartialEq)]
12pub enum ValidationError {
13    IllegalCharacter(&'static str), // A field contains illegal character.
14    TooShortLength(&'static str),   // The length of the value of a field is too short.
15    TooLongLength(&'static str),    // The length of the value of a field is too long.
16    Deplication(&'static str),      // The value of a field is already registered.
17    Requires(&'static str),         // A field must have any value.
18}
19
20type ValidationResult = Result<(), ValidationError>;
21
22impl fmt::Display for ValidationError {
23    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24        match self {
25            Self::IllegalCharacter(msg)
26            | Self::TooShortLength(msg)
27            | Self::TooLongLength(msg)
28            | Self::Deplication(msg)
29            | Self::Requires(msg) => write!(f, "{}", msg),
30        }
31    }
32}
33
34// 設定
35#[derive(Serialize, Deserialize, Debug, Default)]
36pub struct Config {
37    profiles: Vec<Profile>,
38}
39
40impl Config {
41    pub fn new_profile(&mut self, name: &str, secret: &str) -> ValidationResult {
42        self.push_profile(Profile::new(name, secret))
43    }
44
45    fn push_profile(&mut self, profile: Profile) -> ValidationResult {
46        match self.validate_profile(&profile) {
47            Ok(_) => {
48                // TODO: test name duplication
49                self.profiles.push(profile);
50                Ok(())
51            }
52            Err(err) => Err(err),
53        }
54    }
55
56    fn validate_profile(&self, profile: &Profile) -> ValidationResult {
57        if self.find_by_name(&profile.name).is_some() {
58            return Err(ValidationError::Deplication("This name already exists."));
59        }
60
61        profile.is_vaild()
62    }
63
64    // Get the decoded secret value with a profile name.
65    pub fn get_secret_by_name(&self, name: &str) -> Option<Vec<u8>> {
66        if let Some(profile) = self.find_by_name(name) {
67            return profile.get_secret();
68        }
69
70        None
71    }
72
73    // Get borrow profiles
74    pub fn get_profiles(&self) -> &Vec<Profile> {
75        &self.profiles
76    }
77
78    // Remove a profile.
79    pub fn remove_profile(&mut self, name: &str) -> Result<(), String> {
80        let mut index: Option<usize> = None;
81        self.profiles.iter().enumerate().for_each(|(i, profile)| {
82            if profile.name == name {
83                index = Some(i);
84            }
85        });
86
87        match index {
88            Some(i) => {
89                self.profiles.remove(i);
90                Ok(())
91            }
92            _ => Err(format!("Can't find this profile: {}", name)),
93        }
94    }
95
96    fn find_by_name(&self, name: &str) -> Option<&Profile> {
97        self.profiles
98            .iter()
99            .find(|&profile| *profile.get_name() == *name)
100    }
101
102    // Serialize to strings
103    pub fn serialize(&self) -> Result<String, String> {
104        match toml::to_string(&self) {
105            Ok(data) => Ok(data),
106            Err(err) => Err(err.to_string()),
107        }
108    }
109
110    // Deserialize config from strings
111    pub fn deserialize(&mut self, content: &str) -> Result<(), String> {
112        match toml::from_str(content) {
113            Ok(config) => {
114                *self = config;
115                Ok(())
116            }
117            Err(err) => Err(err.to_string()),
118        }
119    }
120}
121
122// MFA の設定
123#[derive(Serialize, Deserialize, Default, Debug)]
124pub struct Profile {
125    name: String,
126    secret: String,
127}
128
129impl Profile {
130    pub fn new(name: &str, secret: &str) -> Self {
131        Profile {
132            name: name.to_string(),
133            secret: secret.to_string(),
134        }
135    }
136
137    pub fn get_name(&self) -> &String {
138        &self.name
139    }
140
141    // returns decoded secret
142    pub fn get_secret(&self) -> Option<Vec<u8>> {
143        base32::decode(base32::Alphabet::RFC4648 { padding: true }, &self.secret)
144    }
145
146    // Validate self fields format.
147    // If validation is failed, returns error type and message.
148    pub fn is_vaild(&self) -> ValidationResult {
149        self.is_valid_name()?;
150
151        self.is_valid_secret()?;
152
153        Ok(())
154    }
155
156    // Validate name format.
157    //
158    // Requires
159    //   - 3~20 characters
160    //   - Alphabet or Number or Symbol (@-_)
161    fn is_valid_name(&self) -> ValidationResult {
162        if self.name.len() < 3 {
163            return Err(ValidationError::TooShortLength(
164                "Name requires at least 3 characters.",
165            ));
166        }
167        if 20 < self.name.len() {
168            return Err(ValidationError::TooLongLength(
169                "Name requires 20 characters or less.",
170            ));
171        }
172
173        // alphabet, number and symbol (@-_)
174        const VALID_NAME_PATTERN: &str = r"^[[[:alnum:]]_@-]+\z";
175        let re = Regex::new(VALID_NAME_PATTERN).unwrap();
176        if !re.is_match(&self.name) {
177            return Err(ValidationError::IllegalCharacter(
178                "Name can contain only alphabet, number and symbol (@-_) .",
179            ));
180        }
181
182        Ok(())
183    }
184
185    // Validate a secret field format.
186    //
187    // Requires
188    //   - doesn't blank
189    fn is_valid_secret(&self) -> ValidationResult {
190        if self.secret.is_empty() {
191            return Err(ValidationError::Requires("Secret must be present."));
192        }
193
194        Ok(())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn serialize_profile() {
204        let profile = Profile::new("test", "secret");
205        let expected = "name = \"test\"\nsecret = \"secret\"\n";
206
207        assert_eq!(toml::to_string(&profile).unwrap(), expected);
208    }
209
210    #[test]
211    fn serialize_config() {
212        let config = Config {
213            profiles: vec![Profile::new("test", "secret")],
214        };
215        let expected = r#"[[profiles]]
216name = "test"
217secret = "secret"
218"#;
219
220        assert_eq!(config.serialize().unwrap(), expected);
221    }
222
223    #[test]
224    fn deserialize_config() {
225        let string_config = "[[profiles]]\nname = \"test\"\nsecret = \"secret\"\n ";
226        let mut config: Config = Default::default();
227
228        config.deserialize(string_config).unwrap();
229
230        assert_eq!(config.profiles.len(), 1);
231        assert_eq!(config.profiles[0].name, "test");
232        assert_eq!(config.profiles[0].secret, "secret");
233    }
234
235    #[test]
236    fn push_profile_validation_when_name_duplicates() {
237        let mut config: Config = Default::default();
238        config.new_profile("test", "a").unwrap();
239        let second_time = config.new_profile("test", "");
240
241        assert!(second_time.is_err());
242    }
243
244    #[test]
245    fn push_profile_validation_when_name_contains_multi_byte_char() {
246        let mut config: Config = Default::default();
247        let result = config.new_profile("あ", "");
248
249        assert_eq!(
250            result,
251            Err(ValidationError::IllegalCharacter(
252                "Name can contain only alphabet, number and symbol (@-_) ."
253            ))
254        );
255    }
256
257    #[test]
258    fn push_profile_validation_when_name_contains_symbols_other_than_hyphen_and_underscore_and_at_sign(
259    ) {
260        let mut config: Config = Default::default();
261        let result = config.new_profile("!# $%&", "");
262
263        assert_eq!(
264            result,
265            Err(ValidationError::IllegalCharacter(
266                "Name can contain only alphabet, number and symbol (@-_) ."
267            ))
268        );
269    }
270
271    #[test]
272    fn push_profile_validation_when_name_contains_approved_symbols() {
273        let mut config: Config = Default::default();
274        let result = config.new_profile("-_@", "secret");
275
276        assert_eq!(result, Ok(()));
277    }
278
279    #[test]
280    fn push_profile_validation_when_name_is_too_short() {
281        let mut config: Config = Default::default();
282        let result = config.new_profile("ab", "");
283
284        assert_eq!(
285            result,
286            Err(ValidationError::TooShortLength(
287                "Name requires at least 3 characters."
288            ))
289        );
290    }
291    #[test]
292    fn push_profile_validation_when_name_is_too_long() {
293        let mut config: Config = Default::default();
294        let result = config.new_profile(&"a".repeat(21), "");
295
296        assert_eq!(
297            result,
298            Err(ValidationError::TooLongLength(
299                "Name requires 20 characters or less."
300            ))
301        );
302    }
303
304    #[test]
305    fn push_profile_validation_when_secret_is_blank() {
306        let mut config: Config = Default::default();
307        let result = config.new_profile("aaa", "");
308
309        assert_eq!(
310            result,
311            Err(ValidationError::Requires("Secret must be present."))
312        );
313    }
314}