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 }]
24}
25
26fn default_enabled() -> bool {
27 true
28}
29
30fn default_minimum_complexity() -> u8 {
31 3
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
36pub struct PasswordsConfig {
37 #[serde(default = "default_enabled")]
39 pub enabled: bool,
40
41 #[serde(default = "default_schemes")]
46 pub schemes: Vec<HashingScheme>,
47
48 #[serde(default = "default_minimum_complexity")]
58 minimum_complexity: u8,
59}
60
61impl Default for PasswordsConfig {
62 fn default() -> Self {
63 Self {
64 enabled: default_enabled(),
65 schemes: default_schemes(),
66 minimum_complexity: default_minimum_complexity(),
67 }
68 }
69}
70
71impl ConfigurationSection for PasswordsConfig {
72 const PATH: Option<&'static str> = Some("passwords");
73
74 fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> {
75 let annotate = |mut error: figment::Error| {
76 error.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned();
77 error.profile = Some(figment::Profile::Default);
78 error.path = vec![Self::PATH.unwrap().to_owned()];
79 Err(error)
80 };
81
82 if !self.enabled {
83 return Ok(());
85 }
86
87 if self.schemes.is_empty() {
88 return annotate(figment::Error::from(
89 "Requires at least one password scheme in the config".to_owned(),
90 ));
91 }
92
93 for scheme in &self.schemes {
94 if scheme.secret.is_some() && scheme.secret_file.is_some() {
95 return annotate(figment::Error::from(
96 "Cannot specify both `secret` and `secret_file`".to_owned(),
97 ));
98 }
99 }
100
101 Ok(())
102 }
103}
104
105impl PasswordsConfig {
106 #[must_use]
108 pub fn enabled(&self) -> bool {
109 self.enabled
110 }
111
112 #[must_use]
115 pub fn minimum_complexity(&self) -> u8 {
116 self.minimum_complexity
117 }
118
119 pub async fn load(
126 &self,
127 ) -> Result<Vec<(u16, Algorithm, Option<u32>, Option<Vec<u8>>)>, anyhow::Error> {
128 let mut schemes: Vec<&HashingScheme> = self.schemes.iter().collect();
129 schemes.sort_unstable_by_key(|a| Reverse(a.version));
130 schemes.dedup_by_key(|a| a.version);
131
132 if schemes.len() != self.schemes.len() {
133 bail!("Multiple password schemes have the same versions");
135 }
136
137 if schemes.is_empty() {
138 bail!("Requires at least one password scheme in the config");
139 }
140
141 let mut mapped_result = Vec::with_capacity(schemes.len());
142
143 for scheme in schemes {
144 let secret = match (&scheme.secret, &scheme.secret_file) {
145 (Some(secret), None) => Some(secret.clone().into_bytes()),
146 (None, Some(secret_file)) => {
147 let secret = tokio::fs::read(secret_file).await?;
148 Some(secret)
149 }
150 (Some(_), Some(_)) => bail!("Cannot specify both `secret` and `secret_file`"),
151 (None, None) => None,
152 };
153
154 mapped_result.push((scheme.version, scheme.algorithm, scheme.cost, secret));
155 }
156
157 Ok(mapped_result)
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
163pub struct HashingScheme {
164 pub version: u16,
167
168 pub algorithm: Algorithm,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
173 #[schemars(default = "default_bcrypt_cost")]
174 pub cost: Option<u32>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
179 pub secret: Option<String>,
180
181 #[serde(skip_serializing_if = "Option::is_none")]
183 #[schemars(with = "Option<String>")]
184 pub secret_file: Option<Utf8PathBuf>,
185}
186
187#[allow(clippy::unnecessary_wraps)]
188fn default_bcrypt_cost() -> Option<u32> {
189 Some(12)
190}
191
192#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
194#[serde(rename_all = "lowercase")]
195pub enum Algorithm {
196 Bcrypt,
198
199 #[default]
201 Argon2id,
202
203 Pbkdf2,
205}