signstar_config/nethsm/admin_credentials.rs
1//! Administrative credentials for [`NetHsm`] backends.
2
3use log::warn;
4use nethsm::{FullCredentials, Passphrase};
5#[cfg(doc)]
6use nethsm::{NetHsm, UserId};
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 admin_credentials::{AdminCredentials, Error},
11 nethsm::{NetHsmConfig, NetHsmUserMapping},
12};
13
14/// Administrative credentials.
15///
16/// Tracks the following credentials and passphrases:
17/// - the backup passphrase of the backend,
18/// - the unlock passphrase of the backend,
19/// - the top-level administrator credentials of the backend,
20/// - the namespace administrator credentials of the backend.
21///
22/// # Note
23///
24/// The unlock and backup passphrase must be at least 10 characters long.
25/// The passphrases of top-level and namespace administrator accounts must be at least 10 characters
26/// long.
27/// The list of top-level administrator credentials must include an account with the username
28/// "admin".
29#[derive(Clone, Debug, Default, Deserialize, Serialize)]
30pub struct NetHsmAdminCredentials {
31 iteration: u32,
32 backup_passphrase: Passphrase,
33 unlock_passphrase: Passphrase,
34 administrators: Vec<FullCredentials>,
35 namespace_administrators: Vec<FullCredentials>,
36}
37
38impl NetHsmAdminCredentials {
39 /// Creates a new [`NetHsmAdminCredentials`] instance.
40 ///
41 /// # Examples
42 ///
43 /// ```
44 /// use nethsm::FullCredentials;
45 /// use signstar_config::nethsm::NetHsmAdminCredentials;
46 ///
47 /// # fn main() -> testresult::TestResult {
48 /// let creds = NetHsmAdminCredentials::new(
49 /// 1,
50 /// "backup-passphrase".parse()?,
51 /// "unlock-passphrase".parse()?,
52 /// vec![FullCredentials::new(
53 /// "admin".parse()?,
54 /// "admin-passphrase".parse()?,
55 /// )],
56 /// vec![FullCredentials::new(
57 /// "ns1~admin".parse()?,
58 /// "ns1-admin-passphrase".parse()?,
59 /// )],
60 /// )?;
61 /// # // the backup passphrase is too short
62 /// # assert!(NetHsmAdminCredentials::new(
63 /// # 1,
64 /// # "short".parse()?,
65 /// # "unlock-passphrase".parse()?,
66 /// # vec![FullCredentials::new("admin".parse()?, "admin-passphrase".parse()?)],
67 /// # vec![FullCredentials::new(
68 /// # "ns1~admin".parse()?,
69 /// # "ns1-admin-passphrase".parse()?,
70 /// # )],
71 /// # ).is_err());
72 /// #
73 /// # // the unlock passphrase is too short
74 /// # assert!(NetHsmAdminCredentials::new(
75 /// # 1,
76 /// # "backup-passphrase".parse()?,
77 /// # "short".parse()?,
78 /// # vec![FullCredentials::new("admin".parse()?, "admin-passphrase".parse()?)],
79 /// # vec![FullCredentials::new(
80 /// # "ns1~admin".parse()?,
81 /// # "ns1-admin-passphrase".parse()?,
82 /// # )],
83 /// # ).is_err());
84 /// #
85 /// # // there is no top-level administrator
86 /// # assert!(NetHsmAdminCredentials::new(
87 /// # 1,
88 /// # "backup-passphrase".parse()?,
89 /// # "unlock-passphrase".parse()?,
90 /// # Vec::new(),
91 /// # vec![FullCredentials::new(
92 /// # "ns1~admin".parse()?,
93 /// # "ns1-admin-passphrase".parse()?,
94 /// # )],
95 /// # ).is_err());
96 /// #
97 /// # // there is no default top-level administrator
98 /// # assert!(NetHsmAdminCredentials::new(
99 /// # 1,
100 /// # "backup-passphrase".parse()?,
101 /// # "unlock-passphrase".parse()?,
102 /// # vec![FullCredentials::new("some".parse()?, "admin-passphrase".parse()?)],
103 /// # vec![FullCredentials::new(
104 /// # "ns1~admin".parse()?,
105 /// # "ns1-admin-passphrase".parse()?,
106 /// # )],
107 /// # ).is_err());
108 /// #
109 /// # // a top-level administrator passphrase is too short
110 /// # assert!(NetHsmAdminCredentials::new(
111 /// # 1,
112 /// # "backup-passphrase".parse()?,
113 /// # "unlock-passphrase".parse()?,
114 /// # vec![FullCredentials::new("admin".parse()?, "short".parse()?)],
115 /// # vec![FullCredentials::new(
116 /// # "ns1~admin".parse()?,
117 /// # "ns1-admin-passphrase".parse()?,
118 /// # )],
119 /// # ).is_err());
120 /// #
121 /// # // a namespace administrator passphrase is too short
122 /// # assert!(NetHsmAdminCredentials::new(
123 /// # 1,
124 /// # "backup-passphrase".parse()?,
125 /// # "unlock-passphrase".parse()?,
126 /// # vec![FullCredentials::new("some".parse()?, "admin-passphrase".parse()?)],
127 /// # vec![FullCredentials::new(
128 /// # "ns1~admin".parse()?,
129 /// # "short".parse()?,
130 /// # )],
131 /// # ).is_err());
132 /// # Ok(())
133 /// # }
134 /// ```
135 pub fn new(
136 iteration: u32,
137 backup_passphrase: Passphrase,
138 unlock_passphrase: Passphrase,
139 administrators: Vec<FullCredentials>,
140 namespace_administrators: Vec<FullCredentials>,
141 ) -> Result<Self, crate::Error> {
142 let admin_credentials = Self {
143 iteration,
144 backup_passphrase,
145 unlock_passphrase,
146 administrators,
147 namespace_administrators,
148 };
149 admin_credentials.validate()?;
150
151 Ok(admin_credentials)
152 }
153
154 /// Returns the iteration.
155 pub fn get_iteration(&self) -> u32 {
156 self.iteration
157 }
158
159 /// Returns the backup passphrase.
160 pub fn get_backup_passphrase(&self) -> &str {
161 self.backup_passphrase.expose_borrowed()
162 }
163
164 /// Returns the unlock passphrase.
165 pub fn get_unlock_passphrase(&self) -> &str {
166 self.unlock_passphrase.expose_borrowed()
167 }
168
169 /// Returns the list of administrators.
170 pub fn get_administrators(&self) -> &[FullCredentials] {
171 &self.administrators
172 }
173
174 /// Returns the default system-wide administrator "admin".
175 ///
176 /// # Errors
177 ///
178 /// Returns an error if no administrative account with the system-wide [`UserId`] "admin" is
179 /// found.
180 pub fn get_default_administrator(&self) -> Result<&FullCredentials, crate::Error> {
181 let Some(first_admin) = self
182 .administrators
183 .iter()
184 .find(|user| user.name.to_string() == "admin")
185 else {
186 return Err(Error::AdministratorNoDefault.into());
187 };
188 Ok(first_admin)
189 }
190
191 /// Returns the list of namespace administrators.
192 pub fn get_namespace_administrators(&self) -> &[FullCredentials] {
193 &self.namespace_administrators
194 }
195
196 /// Returns the list of system-wide administrators, also present in a [`NetHsmConfig`].
197 ///
198 /// Retrieves the list of [`NetHsmUserMapping`] instances that represent system-wide
199 /// administrators from `config`.
200 /// Filters out all [`UserId`]s that cannot be matched and emits warnings for all unmatched
201 /// ones.
202 pub fn administrators_in_config(&self, config: &NetHsmConfig) -> Vec<&FullCredentials> {
203 let user_mappings = config
204 .mappings()
205 .iter()
206 .filter(|mapping| matches!(mapping, NetHsmUserMapping::Admin(..)))
207 .collect::<Vec<_>>();
208 // Only use administrative credentials that are also available in the NetHSM config.
209 {
210 let mut user_list = Vec::new();
211
212 for creds in self.get_administrators() {
213 if !user_mappings
214 .iter()
215 .any(|user_mapping| user_mapping.nethsm_user_ids().contains(&creds.name))
216 {
217 warn!(
218 "The administrative credentials for system-wide administrator {} are skipped because the user is not found in the Signstar configuration.",
219 creds.name
220 );
221 continue;
222 }
223 user_list.push(creds);
224 }
225 // The available user IDs.
226 let available_users = user_list
227 .iter()
228 .map(|creds| &creds.name)
229 .collect::<Vec<_>>();
230
231 let unmatched_config_users = user_mappings
232 .iter()
233 .flat_map(|user_mapping| {
234 user_mapping
235 .nethsm_user_ids()
236 .iter()
237 .filter(|user_id| !available_users.contains(user_id))
238 .cloned()
239 .collect::<Vec<_>>()
240 })
241 .collect::<Vec<_>>();
242 if !unmatched_config_users.is_empty() {
243 warn!(
244 "The following system-wide administrators (R-Administrators) in the Signstar configuration are skipped, because they cannot be found in the provided administrative credentials: {}",
245 unmatched_config_users
246 .iter()
247 .map(ToString::to_string)
248 .collect::<Vec<_>>()
249 .join(", ")
250 );
251 }
252
253 user_list
254 }
255 }
256
257 /// Returns the list of namespace administrators, also present in a [`NetHsmConfig`].
258 ///
259 /// Retrieves the list of [`NetHsmUserMapping`] instances that represent namespace
260 /// administrators from `config`.
261 /// Filters out all [`UserId`]s that cannot be matched and emits warnings for all unmatched
262 /// ones.
263 pub fn namespace_administrators_in_config(
264 &self,
265 config: &NetHsmConfig,
266 ) -> Vec<&FullCredentials> {
267 // The list of namespace administrators.
268 let user_mappings = config
269 .mappings()
270 .iter()
271 .filter(|mapping| {
272 if let NetHsmUserMapping::Admin(user_id) = mapping {
273 user_id.is_namespaced()
274 } else {
275 false
276 }
277 })
278 .collect::<Vec<_>>();
279 // Only use administrative credentials that are also available in the NetHSM config.
280 {
281 let mut user_list = Vec::new();
282
283 for creds in self.get_namespace_administrators() {
284 if !user_mappings
285 .iter()
286 .any(|user_mapping| user_mapping.nethsm_user_ids().contains(&creds.name))
287 {
288 warn!(
289 "The administrative credentials for namespace administrator (N-Administrator) {} are skipped because the user is not found in the Signstar configuration.",
290 creds.name
291 );
292 continue;
293 }
294 user_list.push(creds);
295 }
296 // The available user IDs.
297 let available_users = user_list
298 .iter()
299 .map(|creds| &creds.name)
300 .collect::<Vec<_>>();
301
302 let unmatched_config_users = user_mappings
303 .iter()
304 .flat_map(|user_mapping| {
305 user_mapping
306 .nethsm_user_ids()
307 .iter()
308 .filter(|user_id| !available_users.contains(user_id))
309 .cloned()
310 .collect::<Vec<_>>()
311 })
312 .collect::<Vec<_>>();
313 if !unmatched_config_users.is_empty() {
314 warn!(
315 "The following namespace administrators (N-Administrators) in the Signstar configuration are skipped, because they cannot be found in the provided administrative credentials: {}",
316 unmatched_config_users
317 .iter()
318 .map(ToString::to_string)
319 .collect::<Vec<_>>()
320 .join(", ")
321 );
322 }
323
324 user_list
325 }
326 }
327}
328
329impl AdminCredentials for NetHsmAdminCredentials {
330 /// Validates the [`NetHsmAdminCredentials`].
331 ///
332 /// # Errors
333 ///
334 /// Returns an error if
335 /// - there is no top-level administrator user,
336 /// - the default top-level administrator user (with the name "admin") is missing,
337 /// - a user passphrase is too short,
338 /// - the backup passphrase is too short,
339 /// - or the unlock passphrase is too short.
340 fn validate(&self) -> Result<(), crate::Error> {
341 // there is no top-level administrator user
342 if self.get_administrators().is_empty() {
343 return Err(crate::Error::AdminSecretHandling(
344 Error::AdministratorMissing,
345 ));
346 }
347
348 // there is no top-level administrator user with the name "admin"
349 if !self
350 .get_administrators()
351 .iter()
352 .any(|user| user.name.to_string() == "admin")
353 {
354 return Err(crate::Error::AdminSecretHandling(
355 Error::AdministratorNoDefault,
356 ));
357 }
358
359 let minimum_length: usize = 10;
360
361 // a top-level administrator user passphrase is too short
362 for user in self.get_administrators().iter() {
363 if user.passphrase.expose_borrowed().len() < minimum_length {
364 return Err(crate::Error::AdminSecretHandling(
365 Error::PassphraseTooShort {
366 context: format!("user {}", user.name),
367 minimum_length,
368 },
369 ));
370 }
371 }
372
373 // a namespace administrator user passphrase is too short
374 for user in self.get_namespace_administrators().iter() {
375 if user.passphrase.expose_borrowed().len() < minimum_length {
376 return Err(crate::Error::AdminSecretHandling(
377 Error::PassphraseTooShort {
378 context: format!("user {}", user.name),
379 minimum_length,
380 },
381 ));
382 }
383 }
384
385 // the backup passphrase is too short
386 if self.get_backup_passphrase().len() < minimum_length {
387 return Err(crate::Error::AdminSecretHandling(
388 Error::PassphraseTooShort {
389 context: "backups".to_string(),
390 minimum_length,
391 },
392 ));
393 }
394
395 // the unlock passphrase is too short
396 if self.get_unlock_passphrase().len() < minimum_length {
397 return Err(crate::Error::AdminSecretHandling(
398 Error::PassphraseTooShort {
399 context: "unlocking".to_string(),
400 minimum_length,
401 },
402 ));
403 }
404
405 Ok(())
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use std::{collections::BTreeSet, str::FromStr};
412
413 use nethsm::{Connection, UserId};
414 use rstest::{fixture, rstest};
415 use testresult::TestResult;
416
417 use super::*;
418
419 #[fixture]
420 fn nethsm_admin_credentials() -> TestResult<NetHsmAdminCredentials> {
421 Ok(NetHsmAdminCredentials::new(
422 1,
423 "backup-passphrase".parse()?,
424 "unlock-passphrase".parse()?,
425 vec![
426 FullCredentials::new("admin".parse()?, "admin-passphrase".parse()?),
427 FullCredentials::new("admin2".parse()?, "admin2-passphrase".parse()?),
428 ],
429 vec![
430 FullCredentials::new("ns1~admin".parse()?, "ns1~admin-passphrase".parse()?),
431 FullCredentials::new("ns1~admin2".parse()?, "ns1~admin2-passphrase".parse()?),
432 ],
433 )?)
434 }
435
436 #[fixture]
437 fn nethsm_config() -> TestResult<NetHsmConfig> {
438 Ok(NetHsmConfig::new(
439 BTreeSet::from_iter([Connection::new(
440 "https://nethsm1.example.org/".parse()?,
441 nethsm::ConnectionSecurity::Unsafe,
442 )]),
443 BTreeSet::from_iter([
444 NetHsmUserMapping::Admin("admin".parse()?),
445 NetHsmUserMapping::Admin("ns1~admin".parse()?),
446 ]),
447 )?)
448 }
449
450 #[rstest]
451 fn nethsm_admin_credentials_administrators_in_config(
452 nethsm_admin_credentials: TestResult<NetHsmAdminCredentials>,
453 nethsm_config: TestResult<NetHsmConfig>,
454 ) -> TestResult {
455 let nethsm_admin_credentials = nethsm_admin_credentials?;
456 let nethsm_config = nethsm_config?;
457 let users = nethsm_admin_credentials
458 .administrators_in_config(&nethsm_config)
459 .iter()
460 .map(|creds| creds.name.clone())
461 .collect::<Vec<_>>();
462
463 assert_eq!(users, vec![UserId::from_str("admin")?]);
464
465 Ok(())
466 }
467
468 #[rstest]
469 fn nethsm_admin_credentials_namespace_administrators_in_config(
470 nethsm_admin_credentials: TestResult<NetHsmAdminCredentials>,
471 nethsm_config: TestResult<NetHsmConfig>,
472 ) -> TestResult {
473 let nethsm_admin_credentials = nethsm_admin_credentials?;
474 let nethsm_config = nethsm_config?;
475 let users = nethsm_admin_credentials
476 .namespace_administrators_in_config(&nethsm_config)
477 .iter()
478 .map(|creds| creds.name.clone())
479 .collect::<Vec<_>>();
480
481 assert_eq!(users, vec![UserId::from_str("ns1~admin")?]);
482
483 Ok(())
484 }
485}