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};
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}