mas_config/sections/
passwords.rs1use std::cmp::Reverse;
8
9use anyhow::bail;
10use camino::Utf8PathBuf;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::ConfigurationSection;
15
16fn default_schemes() -> Vec<HashingScheme> {
17 vec![HashingScheme {
18 version: 1,
19 algorithm: Algorithm::default(),
20 cost: None,
21 secret: None,
22 secret_file: None,
23 unicode_normalization: false,
24 }]
25}
26
27fn default_enabled() -> bool {
28 true
29}
30
31fn default_minimum_complexity() -> u8 {
32 3
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
37pub struct PasswordsConfig {
38 #[serde(default = "default_enabled")]
40 pub enabled: bool,
41
42 #[serde(default = "default_schemes")]
47 pub schemes: Vec<HashingScheme>,
48
49 #[serde(default = "default_minimum_complexity")]
59 minimum_complexity: u8,
60}
61
62impl Default for PasswordsConfig {
63 fn default() -> Self {
64 Self {
65 enabled: default_enabled(),
66 schemes: default_schemes(),
67 minimum_complexity: default_minimum_complexity(),
68 }
69 }
70}
71
72impl ConfigurationSection for PasswordsConfig {
73 const PATH: Option<&'static str> = Some("passwords");
74
75 fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
76 let annotate = |mut error: figment::Error| {
77 error.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned();
78 error.profile = Some(figment::Profile::Default);
79 error.path = vec![Self::PATH.unwrap().to_owned()];
80 Err(error)
81 };
82
83 if !self.enabled {
84 return Ok(());
86 }
87
88 if self.schemes.is_empty() {
89 return annotate(figment::Error::from(
90 "Requires at least one password scheme in the config".to_owned(),
91 ));
92 }
93
94 for scheme in &self.schemes {
95 if scheme.secret.is_some() && scheme.secret_file.is_some() {
96 return annotate(figment::Error::from(
97 "Cannot specify both `secret` and `secret_file`".to_owned(),
98 ));
99 }
100 }
101
102 Ok(())
103 }
104}
105
106impl PasswordsConfig {
107 #[must_use]
109 pub fn enabled(&self) -> bool {
110 self.enabled
111 }
112
113 #[must_use]
116 pub fn minimum_complexity(&self) -> u8 {
117 self.minimum_complexity
118 }
119
120 pub async fn load(
127 &self,
128 ) -> Result<Vec<(u16, Algorithm, Option<u32>, Option<Vec<u8>>, bool)>, anyhow::Error> {
129 let mut schemes: Vec<&HashingScheme> = self.schemes.iter().collect();
130 schemes.sort_unstable_by_key(|a| Reverse(a.version));
131 schemes.dedup_by_key(|a| a.version);
132
133 if schemes.len() != self.schemes.len() {
134 bail!("Multiple password schemes have the same versions");
136 }
137
138 if schemes.is_empty() {
139 bail!("Requires at least one password scheme in the config");
140 }
141
142 let mut mapped_result = Vec::with_capacity(schemes.len());
143
144 for scheme in schemes {
145 let secret = match (&scheme.secret, &scheme.secret_file) {
146 (Some(secret), None) => Some(secret.clone().into_bytes()),
147 (None, Some(secret_file)) => {
148 let secret = tokio::fs::read(secret_file).await?;
149 Some(secret)
150 }
151 (Some(_), Some(_)) => bail!("Cannot specify both `secret` and `secret_file`"),
152 (None, None) => None,
153 };
154
155 mapped_result.push((
156 scheme.version,
157 scheme.algorithm,
158 scheme.cost,
159 secret,
160 scheme.unicode_normalization,
161 ));
162 }
163
164 Ok(mapped_result)
165 }
166}
167
168#[allow(clippy::trivially_copy_pass_by_ref)]
169const fn is_default_false(value: &bool) -> bool {
170 !*value
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
175pub struct HashingScheme {
176 pub version: u16,
179
180 pub algorithm: Algorithm,
182
183 #[serde(default, skip_serializing_if = "is_default_false")]
189 pub unicode_normalization: bool,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 #[schemars(default = "default_bcrypt_cost")]
194 pub cost: Option<u32>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
199 pub secret: Option<String>,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 #[schemars(with = "Option<String>")]
204 pub secret_file: Option<Utf8PathBuf>,
205}
206
207#[allow(clippy::unnecessary_wraps)]
208fn default_bcrypt_cost() -> Option<u32> {
209 Some(12)
210}
211
212#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
214#[serde(rename_all = "lowercase")]
215pub enum Algorithm {
216 Bcrypt,
218
219 #[default]
221 Argon2id,
222
223 Pbkdf2,
225}