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