Skip to main content

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}