signstar_config/nethsm/
state.rs

1//! State representation for [`NetHsm`] backends and Signstar configurations.
2//!
3//! Allows to create state representations of users ([`UserState`]) and keys ([`KeyState`] and
4//! [`KeyCertificateState`]) for [`NetHsm`] backends and Signstar configurations ([`State`]).
5//!
6//! Each [`State`] may be compared against another which may lead to [`StateComparisonErrors`]
7//! returning an error, that describes the discrepancies between the two.
8
9use std::fmt::Display;
10
11use log::debug;
12#[cfg(doc)]
13use nethsm::NetHsm;
14use nethsm::{
15    CryptographicKeyContext,
16    KeyId,
17    KeyMechanism,
18    KeyType,
19    NamespaceId,
20    UserId,
21    UserRole,
22};
23use nethsm_config::{FilterUserKeys, HermeticParallelConfig};
24#[cfg(doc)]
25use pgp::composed::SignedPublicKey;
26use strum::IntoStaticStr;
27
28use crate::NetHsmBackendError;
29
30/// An error that may occur when comparing two [`State`] structs.
31#[derive(Debug, thiserror::Error)]
32pub enum StateComparisonError {
33    /// Two key states mismatch.
34    #[error(
35        "Key mismatch:\n{} (A) => {}\n{} (B) => {}",
36        self_key.state_type, self_key.key_state, other_key.state_type, other_key.key_state
37    )]
38    KeyStateMismatch {
39        /// The state of the left hand side key.
40        self_key: KeyStateType,
41
42        /// The state of the right hand side key.
43        other_key: KeyStateType,
44    },
45
46    /// The key states in one state type can not be matched by those in another.
47    #[error(
48        "Keys missing in {other_state_type} (B), but present in {state_type} (A):\n{}",
49        key_states.iter().map(|state| state.to_string()).collect::<Vec<String>>().join("\n"))]
50    UnmatchedKeyStates {
51        /// The type of state the unmatched key states belong to.
52        state_type: StateType,
53
54        /// The type of state in which the key states are not present.
55        other_state_type: StateType,
56
57        /// The key states that are present in `state_type` but not in `other_state_type`.
58        key_states: Vec<KeyState>,
59    },
60
61    /// The user states in one state type can not be matched by those in another.
62    #[error(
63        "Users missing in {other_state_type} (B), but present in {state_type} (A):\n{}",
64        user_states.iter().map(|state| state.to_string()).collect::<Vec<String>>().join("\n"))]
65    UnmatchedUserStates {
66        /// The type of state the unmatched user states belong to.
67        state_type: StateType,
68
69        /// The type of state in which the user states are not present.
70        other_state_type: StateType,
71
72        /// The user states that are present in `state_type` but not in `other_state_type`.
73        user_states: Vec<UserState>,
74    },
75
76    /// Two user states mismatch.
77    #[error(
78        "User mismatch:\n{} (A) => {}\n{} (B) => {}",
79        self_user.state_type, self_user.user_state, other_user.state_type, other_user.user_state
80    )]
81    UserStateMismatch {
82        /// The state of the left hand side user.
83        self_user: UserStateType,
84
85        /// The state of the right hand side user.
86        other_user: UserStateType,
87    },
88}
89
90/// Zero or more errors that may occur when comparing two [`State`] structs.
91#[derive(Debug, Default)]
92pub struct StateComparisonErrors {
93    errors: Vec<StateComparisonError>,
94}
95
96impl StateComparisonErrors {
97    /// Creates a new [`StateComparisonErrors`].
98    pub fn new() -> Self {
99        Default::default()
100    }
101
102    /// Adds a [`StateComparisonError`] to `self`.
103    pub fn add(&mut self, elem: StateComparisonError) {
104        self.errors.push(elem)
105    }
106
107    /// Checks if errors have been appended and consumes `self`.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if one or more errors have been appended.
112    pub fn check(self) -> Result<(), crate::Error> {
113        if !self.errors.is_empty() {
114            return Err(NetHsmBackendError::CompareStates(self).into());
115        }
116
117        Ok(())
118    }
119}
120
121impl Display for StateComparisonErrors {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        for error in self.errors.iter() {
124            writeln!(f, "{error}")?;
125        }
126        Ok(())
127    }
128}
129
130/// The state of a user.
131///
132/// State may be derived e.g. from a [`NetHsm`] backend or a Signstar configuration file.
133#[derive(Clone, Debug, Eq, PartialEq)]
134pub struct UserState {
135    /// The name of the user.
136    pub name: UserId,
137    /// The role of the user.
138    pub role: UserRole,
139    /// The zero or more tags assigned to the user.
140    pub tags: Vec<String>,
141}
142
143impl Display for UserState {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "{} (role: {}", self.name, self.role)?;
146        if !self.tags.is_empty() {
147            write!(f, "; tags: {}", self.tags.join(", "))?;
148        }
149        write!(f, ")")?;
150
151        Ok(())
152    }
153}
154
155/// The state of a key certificate.
156///
157/// Key certificates carry information on the context in which a key is used.
158/// They can be derived e.g. from NetHSM backends or Signstar configuration files.
159#[derive(Clone, Debug, Eq, PartialEq)]
160pub enum KeyCertificateState {
161    /// A [`CryptographicKeyContext`] describing the context in which a certificate is used.
162    KeyContext(CryptographicKeyContext),
163
164    /// There is no key certificate for the key.
165    Empty,
166
167    /// A key certificate could not be retrieved due to an error.
168    Error {
169        /// A string containing the error message.
170        message: String,
171    },
172
173    /// The key certificate cannot be turned into a [`CryptographicKeyContext`].
174    NotACryptographicKeyContext {
175        /// A message explaining that and why the [`CryptographicKeyContext`] cannot be created.
176        message: String,
177    },
178
179    /// The key certificate cannot be turned into a [`SignedPublicKey`] (an OpenPGP certificate).
180    NotAnOpenPgpCertificate {
181        /// A message explaining why the key certificate cannot be converted to a
182        /// [`SignedPublicKey`].
183        message: String,
184    },
185}
186
187impl Display for KeyCertificateState {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            Self::KeyContext(context) => write!(f, "{context}"),
191            Self::Empty => write!(f, "Empty"),
192            Self::Error { message } => {
193                write!(f, "Error retrieving key certificate - {message}")
194            }
195            Self::NotACryptographicKeyContext { message } => {
196                write!(f, "Not a cryptographic key context - \"{message}\"")
197            }
198            Self::NotAnOpenPgpCertificate { message } => {
199                write!(f, "Not an OpenPGP certificate - \"{message}\"")
200            }
201        }
202    }
203}
204
205/// The state of a key.
206///
207/// State may be derived e.g. from a [`NetHsm`] backend or a Signstar configuration file.
208#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct KeyState {
210    /// The name of the key.
211    pub name: KeyId,
212    /// The optional namespace the key is used in.
213    pub namespace: Option<NamespaceId>,
214    /// The zero or more tags assigned to the key.
215    pub tags: Vec<String>,
216    /// The key type of the key.
217    pub key_type: KeyType,
218    /// The mechanisms supported by the key.
219    pub mechanisms: Vec<KeyMechanism>,
220    /// The context in which the key is used.
221    pub key_cert_state: KeyCertificateState,
222}
223
224impl Display for KeyState {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        write!(f, "{} (", self.name)?;
227        if let Some(namespace) = self.namespace.as_ref() {
228            write!(f, "namespace: {namespace}; ")?;
229        }
230        if !self.tags.is_empty() {
231            write!(f, "tags: {}; ", self.tags.join(", "))?;
232        }
233        write!(f, "type: {}; ", self.key_type)?;
234        write!(
235            f,
236            "mechanisms: {}; ",
237            self.mechanisms
238                .iter()
239                .map(|mechanism| mechanism.to_string())
240                .collect::<Vec<String>>()
241                .join(", ")
242        )?;
243        write!(f, "context: {}", self.key_cert_state)?;
244        write!(f, ")")?;
245
246        Ok(())
247    }
248}
249
250/// Indicator for [`State`] to distinguish what its data belongs to.
251#[derive(Clone, Copy, Debug, strum::Display, Eq, IntoStaticStr, PartialEq)]
252pub enum StateType {
253    /// A [`NetHsm`] backend.
254    #[strum(to_string = "NetHSM")]
255    NetHsm,
256
257    /// A Signstar configuration file.
258    #[strum(to_string = "Signstar configuration")]
259    SignstarConfig,
260}
261
262/// A wrapper around a [`StateType`] and a [`UserState`].
263///
264/// Describes the state of a user for a given type of state (e.g. on a NetHSM backend or in a
265/// Signstar configuration file).
266#[derive(Clone, Debug)]
267pub struct UserStateType {
268    /// The type of state the user state belongs to.
269    pub state_type: StateType,
270    /// The state of the user.
271    pub user_state: UserState,
272}
273
274impl Display for UserStateType {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        write!(f, "{} (user): {}", self.state_type, self.user_state)
277    }
278}
279
280/// A wrapper around a [`StateType`] and a [`KeyState`].
281///
282/// Describes the state of a key for a given type of state (e.g. on a NetHSM backend or in a
283/// Signstar configuration file).
284#[derive(Clone, Debug)]
285pub struct KeyStateType {
286    /// The type of state the user state belongs to.
287    pub state_type: StateType,
288    /// The state of the user.
289    pub key_state: KeyState,
290}
291
292impl Display for KeyStateType {
293    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294        write!(f, "{} (key): {}", self.state_type, self.key_state)
295    }
296}
297
298/// The state of a users and keys for a given type of state.
299///
300/// Tracks a list of [`UserState`]s and a list of [`KeyState`]s for a type of state ([`StateType`]).
301#[derive(Clone, Debug)]
302pub struct State {
303    /// The indicator for what the data belongs to.
304    pub state_type: StateType,
305    /// The state of all users on the backend.
306    pub users: Vec<UserState>,
307    /// The state of all keys on the backend.
308    pub keys: Vec<KeyState>,
309}
310
311impl State {
312    /// Compares `self` with another [`State`].
313    ///
314    /// Compares all components of each [`State`] and returns without error if all components
315    /// match.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if one or more components of `self` and `other` do not match.
320    pub fn compare(&self, other: &State) -> Result<(), crate::Error> {
321        debug!(
322            "Create state diff of {} ({} users, {} keys) and {} ({} users, {} keys)",
323            self.state_type,
324            self.users.len(),
325            self.keys.len(),
326            other.state_type,
327            other.users.len(),
328            other.keys.len()
329        );
330        let mut errors = StateComparisonErrors::new();
331
332        // Create a list of all unmatched user states in `self.users` and the matched ones in
333        // `other.users`.
334        let (unmatched_self_users, matched_other_users) = {
335            let mut unmatched_self_users = Vec::new();
336            let mut matched_other_users = Vec::new();
337
338            // Get all user states in `self.users` that are also in `other.users` and compare them.
339            // Create a `StateComparisonError::UserStateMismatch` for all mismatching user states.
340            for self_user in self.users.iter() {
341                let Some(other_user) = other.users.iter().find(|user| user.name == self_user.name)
342                else {
343                    unmatched_self_users.push(self_user);
344                    continue;
345                };
346
347                matched_other_users.push(other_user);
348                if self_user != other_user {
349                    errors.add(StateComparisonError::UserStateMismatch {
350                        self_user: UserStateType {
351                            state_type: self.state_type,
352                            user_state: self_user.clone(),
353                        },
354                        other_user: UserStateType {
355                            state_type: other.state_type,
356                            user_state: other_user.clone(),
357                        },
358                    });
359                    continue;
360                }
361            }
362
363            (unmatched_self_users, matched_other_users)
364        };
365
366        // If there are unmatched user states `self.users`, add a dedicated error for them.
367        if !unmatched_self_users.is_empty() {
368            errors.add(StateComparisonError::UnmatchedUserStates {
369                state_type: self.state_type,
370                other_state_type: other.state_type,
371                user_states: unmatched_self_users.into_iter().cloned().collect(),
372            });
373        }
374
375        {
376            // If there are unmatched user states for `other.users`, add a dedicated error for them.
377            let mut unmatched_other_users = Vec::new();
378            if matched_other_users.len() != other.users.len() {
379                for other_user in other.users.iter() {
380                    if !matched_other_users.contains(&other_user) {
381                        unmatched_other_users.push(other_user);
382                        // continue;
383                    };
384                }
385            }
386            if !unmatched_other_users.is_empty() {
387                errors.add(StateComparisonError::UnmatchedUserStates {
388                    state_type: other.state_type,
389                    other_state_type: self.state_type,
390                    user_states: unmatched_other_users.into_iter().cloned().collect(),
391                });
392            }
393        }
394
395        // Create a list of all unmatched key states in `self.keys` and the matched ones in
396        // `other.keys`.
397        let (unmatched_self_keys, matched_other_keys) =
398            {
399                let mut unmatched_self_keys = Vec::new();
400                let mut matched_other_keys = Vec::new();
401
402                // Get all key states in `self.keys` that are also in `other.keys` and compare them.
403                // Create a `StateComparisonError::KeyStateMismatch` for all mismatching key states.
404                for self_key in self.keys.iter() {
405                    let Some(other_key) = other.keys.iter().find(|key| {
406                        key.name == self_key.name && key.namespace == self_key.namespace
407                    }) else {
408                        unmatched_self_keys.push(self_key);
409                        continue;
410                    };
411
412                    matched_other_keys.push(other_key);
413
414                    // Note: If `self_key.key_cert_state` or `other_key.key_cert_state` has a
415                    // `CryptographicKeyContext::Raw`, it cannot have a `KeyCertificateState` other
416                    // than `KeyCertificateState::Empty`, as "raw" keys do not have a certificate.
417                    if (matches!(self_key.key_cert_state, KeyCertificateState::Empty)
418                        && matches!(
419                            other_key.key_cert_state,
420                            KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
421                        ))
422                        || (matches!(
423                            self_key.key_cert_state,
424                            KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
425                        ) && matches!(other_key.key_cert_state, KeyCertificateState::Empty))
426                    {
427                        continue;
428                    }
429
430                    if self_key != other_key {
431                        errors.add(StateComparisonError::KeyStateMismatch {
432                            self_key: KeyStateType {
433                                state_type: self.state_type,
434                                key_state: self_key.clone(),
435                            },
436                            other_key: KeyStateType {
437                                state_type: other.state_type,
438                                key_state: other_key.clone(),
439                            },
440                        })
441                    }
442                }
443
444                (unmatched_self_keys, matched_other_keys)
445            };
446
447        // If there are unmatched key states in `self.keys`, add a dedicated error for them.
448        if !unmatched_self_keys.is_empty() {
449            errors.add(StateComparisonError::UnmatchedKeyStates {
450                state_type: self.state_type,
451                other_state_type: other.state_type,
452                key_states: unmatched_self_keys.into_iter().cloned().collect(),
453            });
454        }
455
456        {
457            // If there are unmatched key states in `other.keys`, add a dedicated error for them.
458            let mut unmatched_other_keys = Vec::new();
459            if matched_other_keys.len() != other.keys.len() {
460                for other_key in other.keys.iter() {
461                    if !matched_other_keys.contains(&other_key) {
462                        unmatched_other_keys.push(other_key);
463                        continue;
464                    };
465                }
466            }
467            if !unmatched_other_keys.is_empty() {
468                errors.add(StateComparisonError::UnmatchedKeyStates {
469                    state_type: other.state_type,
470                    other_state_type: self.state_type,
471                    key_states: unmatched_other_keys.into_iter().cloned().collect(),
472                });
473            }
474        }
475
476        errors.check()?;
477
478        Ok(())
479    }
480}
481
482impl From<&HermeticParallelConfig> for State {
483    /// Creates a [`State`] from [`HermeticParallelConfig`] reference.
484    fn from(value: &HermeticParallelConfig) -> Self {
485        let users: Vec<UserState> = value
486            .iter_user_mappings()
487            .flat_map(|mapping| {
488                mapping
489                    .get_nethsm_user_role_and_tags()
490                    .iter()
491                    .map(|(name, role, tags)| UserState {
492                        name: name.clone(),
493                        role: *role,
494                        tags: tags.clone(),
495                    })
496                    .collect::<Vec<UserState>>()
497            })
498            .collect();
499        let keys: Vec<KeyState> = value
500            .iter_user_mappings()
501            .flat_map(|mapping| {
502                mapping
503                    .get_nethsm_user_key_and_tag(FilterUserKeys::All)
504                    .iter()
505                    .map(|(user_id, key_setup, tag)| KeyState {
506                        name: key_setup.get_key_id(),
507                        namespace: user_id.namespace().cloned(),
508                        tags: vec![tag.to_string()],
509                        key_type: key_setup.get_key_type(),
510                        mechanisms: key_setup.get_key_mechanisms(),
511                        key_cert_state: KeyCertificateState::KeyContext(
512                            key_setup.get_key_context(),
513                        ),
514                    })
515                    .collect::<Vec<KeyState>>()
516            })
517            .collect();
518
519        Self {
520            state_type: StateType::SignstarConfig,
521            users,
522            keys,
523        }
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use log::{LevelFilter, debug};
530    use nethsm::OpenPgpUserIdList;
531    use rstest::rstest;
532    use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
533    use testresult::TestResult;
534
535    use super::*;
536
537    /// Initializes a global [`TermLogger`].
538    fn init_logger() {
539        if TermLogger::init(
540            LevelFilter::Trace,
541            Config::default(),
542            TerminalMode::Stderr,
543            ColorChoice::Auto,
544        )
545        .is_err()
546        {
547            debug!("Not initializing another logger, as one is initialized already.");
548        }
549    }
550
551    /// Ensures that [`UserState::to_string`] shows correctly.
552    #[rstest]
553    #[case(
554        UserState{
555            name: "testuser".parse()?,
556            role: UserRole::Operator,
557            tags: vec!["tag1".to_string(), "tag2".to_string()]
558        },
559        "testuser (role: Operator; tags: tag1, tag2)",
560    )]
561    #[case(
562        UserState{
563            name: "testuser".parse()?,
564            role: UserRole::Operator,
565            tags: Vec::new(),
566        },
567        "testuser (role: Operator)",
568    )]
569    #[case(
570        UserState{
571            name: "testuser".parse()?,
572            role: UserRole::Metrics,
573            tags: Vec::new(),
574        },
575        "testuser (role: Metrics)",
576    )]
577    #[case(
578        UserState{
579            name: "testuser".parse()?,
580            role: UserRole::Backup,
581            tags: Vec::new(),
582        },
583        "testuser (role: Backup)",
584    )]
585    #[case(
586        UserState{name:
587            "testuser".parse()?,
588            role: UserRole::Administrator,
589            tags: Vec::new(),
590        },
591        "testuser (role: Administrator)",
592    )]
593    fn user_state_to_string(#[case] user_state: UserState, #[case] expected: &str) -> TestResult {
594        init_logger();
595
596        assert_eq!(user_state.to_string(), expected);
597        Ok(())
598    }
599
600    /// Ensures that [`KeyState::to_string`] shows correctly.
601    #[rstest]
602    #[case::namespaced_key_with_openpgp_v4_cert(
603        KeyState{
604            name: "key1".parse()?,
605            namespace: Some("ns1".parse()?),
606            tags: vec!["tag1".to_string(), "tag2".to_string()],
607            key_type: KeyType::Curve25519,
608            mechanisms: vec![KeyMechanism::EdDsaSignature],
609            key_cert_state: KeyCertificateState::KeyContext(
610                CryptographicKeyContext::OpenPgp {
611                    user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
612                    version: nethsm::OpenPgpVersion::V4,
613                })
614        },
615        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: \"Foobar McFooface <foobar@mcfooface.org>\"))",
616    )]
617    #[case::namespaced_key_with_raw_cert(
618        KeyState{
619            name: "key1".parse()?,
620            namespace: Some("ns1".parse()?),
621            tags: vec!["tag1".to_string(), "tag2".to_string()],
622            key_type: KeyType::Curve25519,
623            mechanisms: vec![KeyMechanism::EdDsaSignature],
624            key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
625        },
626        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
627    )]
628    #[case::namespaced_key_with_no_cert(
629        KeyState{
630            name: "key1".parse()?,
631            namespace: Some("ns1".parse()?),
632            tags: vec!["tag1".to_string(), "tag2".to_string()],
633            key_type: KeyType::Curve25519,
634            mechanisms: vec![KeyMechanism::EdDsaSignature],
635            key_cert_state: KeyCertificateState::Empty
636        },
637        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Empty)",
638    )]
639    #[case::namespaced_key_with_cert_error(
640        KeyState{
641            name: "key1".parse()?,
642            namespace: Some("ns1".parse()?),
643            tags: vec!["tag1".to_string(), "tag2".to_string()],
644            key_type: KeyType::Curve25519,
645            mechanisms: vec![KeyMechanism::EdDsaSignature],
646            key_cert_state: KeyCertificateState::Error { message: "the dog ate it".to_string() }
647        },
648        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Error retrieving key certificate - the dog ate it)",
649    )]
650    #[case::namespaced_key_with_not_a_cert_context(
651        KeyState{
652            name: "key1".parse()?,
653            namespace: Some("ns1".parse()?),
654            tags: vec!["tag1".to_string(), "tag2".to_string()],
655            key_type: KeyType::Curve25519,
656            mechanisms: vec![KeyMechanism::EdDsaSignature],
657            key_cert_state: KeyCertificateState::NotACryptographicKeyContext { message: "failed to convert".to_string() }
658        },
659        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Not a cryptographic key context - \"failed to convert\")",
660    )]
661    #[case::namespaced_key_with_not_an_openpgp_cert(
662        KeyState{
663            name: "key1".parse()?,
664            namespace: Some("ns1".parse()?),
665            tags: vec!["tag1".to_string(), "tag2".to_string()],
666            key_type: KeyType::Curve25519,
667            mechanisms: vec![KeyMechanism::EdDsaSignature],
668            key_cert_state: KeyCertificateState::NotAnOpenPgpCertificate { message: "it's a blob".to_string() }
669        },
670        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Not an OpenPGP certificate - \"it's a blob\")",
671    )]
672    #[case::system_wide_key_with_no_cert_and_no_tags_and_raw_cert(
673        KeyState{
674            name: "key1".parse()?,
675            namespace: None,
676            tags: Vec::new(),
677            key_type: KeyType::Curve25519,
678            mechanisms: vec![KeyMechanism::EdDsaSignature],
679            key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
680        },
681        "key1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
682    )]
683    fn key_state_to_string(#[case] key_state: KeyState, #[case] expected: &str) -> TestResult {
684        init_logger();
685
686        assert_eq!(key_state.to_string(), expected);
687        Ok(())
688    }
689
690    /// Ensures that [`StateType::to_string`] shows correctly.
691    #[rstest]
692    #[case(StateType::NetHsm, "NetHSM")]
693    #[case(StateType::SignstarConfig, "Signstar configuration")]
694    fn state_type_display(#[case] state_type: StateType, #[case] expected: &str) -> TestResult {
695        init_logger();
696
697        assert_eq!(state_type.to_string(), expected);
698        Ok(())
699    }
700
701    /// Ensures that [`KeyStateType::to_string`] shows correctly.
702    #[rstest]
703    #[case(
704        KeyStateType{
705            state_type: StateType::NetHsm,
706            key_state: KeyState{
707                name: "key1".parse()?,
708                namespace: None,
709                tags: Vec::new(),
710                key_type: KeyType::Curve25519,
711                mechanisms: vec![KeyMechanism::EdDsaSignature],
712                key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
713            },
714        },
715        "NetHSM (key): key1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
716    )]
717    #[case(
718        KeyStateType{
719            state_type: StateType::SignstarConfig,
720            key_state: KeyState{
721                name: "key1".parse()?,
722                namespace: None,
723                tags: Vec::new(),
724                key_type: KeyType::Curve25519,
725                mechanisms: vec![KeyMechanism::EdDsaSignature],
726                key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
727            },
728        },
729        "Signstar configuration (key): key1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
730    )]
731    fn key_state_type_display(
732        #[case] state_type: KeyStateType,
733        #[case] expected: &str,
734    ) -> TestResult {
735        init_logger();
736
737        assert_eq!(state_type.to_string(), expected);
738        Ok(())
739    }
740
741    /// Ensures that [`UserStateType::to_string`] shows correctly.
742    #[rstest]
743    #[case(
744        UserStateType{
745            state_type: StateType::NetHsm,
746            user_state: UserState{
747                name: "testuser".parse()?,
748                role: UserRole::Administrator,
749                tags: Vec::new(),
750            }
751        },
752        "NetHSM (user): testuser (role: Administrator)",
753    )]
754    #[case(
755        UserStateType{
756            state_type: StateType::SignstarConfig,
757            user_state: UserState{
758                name: "testuser".parse()?,
759                role: UserRole::Administrator,
760                tags: Vec::new(),
761            },
762        },
763        "Signstar configuration (user): testuser (role: Administrator)",
764    )]
765    fn user_state_type_display(
766        #[case] state_type: UserStateType,
767        #[case] expected: &str,
768    ) -> TestResult {
769        init_logger();
770
771        assert_eq!(state_type.to_string(), expected);
772        Ok(())
773    }
774
775    /// Ensures that [`State::compare`] successfully compares [`State`] containing the same data.
776    #[rstest]
777    #[case::empty(
778        State {
779            state_type: StateType::SignstarConfig,
780            users: Vec::new(),
781            keys: Vec::new(),
782        },
783        State {
784            state_type: StateType::NetHsm,
785            users: Vec::new(),
786            keys: Vec::new(),
787        },
788    )]
789    #[case::with_users_and_keys(
790        State {
791            state_type: StateType::SignstarConfig,
792            users: vec![
793                UserState{
794                    name: "operator1".parse()?,
795                    role: UserRole::Operator,
796                    tags: vec!["tag1".to_string()]
797                },
798                UserState{
799                    name: "admin".parse()?,
800                    role: UserRole::Administrator,
801                    tags: Vec::new(),
802                },
803            ],
804            keys: vec![
805                KeyState{
806                    name: "key1".parse()?,
807                    namespace: None,
808                    tags: vec!["tag1".to_string()],
809                    key_type: KeyType::Curve25519,
810                    mechanisms: vec![KeyMechanism::EdDsaSignature],
811                    key_cert_state: KeyCertificateState::KeyContext(
812                        CryptographicKeyContext::OpenPgp {
813                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
814                            version: nethsm::OpenPgpVersion::V4,
815                        }
816                    )
817                },
818            ],
819        },
820        State {
821            state_type: StateType::NetHsm,
822            users: vec![
823                UserState{
824                    name: "operator1".parse()?,
825                    role: UserRole::Operator,
826                    tags: vec!["tag1".to_string()]
827                },
828                UserState{
829                    name: "admin".parse()?,
830                    role: UserRole::Administrator,
831                    tags: Vec::new(),
832                },
833            ],
834            keys: vec![
835                KeyState{
836                    name: "key1".parse()?,
837                    namespace: None,
838                    tags: vec!["tag1".to_string()],
839                    key_type: KeyType::Curve25519,
840                    mechanisms: vec![KeyMechanism::EdDsaSignature],
841                    key_cert_state: KeyCertificateState::KeyContext(
842                        CryptographicKeyContext::OpenPgp {
843                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
844                            version: nethsm::OpenPgpVersion::V4,
845                        }
846                    )
847                },
848            ],
849        },
850    )]
851    fn state_compare_succeeds(#[case] state_a: State, #[case] state_b: State) -> TestResult {
852        init_logger();
853
854        let compare_result = state_a.compare(&state_b);
855
856        if let Err(error) = compare_result {
857            panic!("Comparison should have succeeded but failed:\n{error}")
858        }
859
860        Ok(())
861    }
862
863    /// Ensures that [`State::compare`] fails on [`State`] containing differing data.
864    #[rstest]
865    #[case::one_empty(
866        State {
867            state_type: StateType::SignstarConfig,
868            users: vec![
869                UserState{
870                    name: "operator1".parse()?,
871                    role: UserRole::Operator,
872                    tags: vec!["tag1".to_string()]
873                },
874                UserState{
875                    name: "admin".parse()?,
876                    role: UserRole::Administrator,
877                    tags: Vec::new(),
878                },
879            ],
880            keys: vec![
881                KeyState{
882                    name: "key1".parse()?,
883                    namespace: None,
884                    tags: vec!["tag1".to_string()],
885                    key_type: KeyType::Curve25519,
886                    mechanisms: vec![KeyMechanism::EdDsaSignature],
887                    key_cert_state: KeyCertificateState::KeyContext(
888                        CryptographicKeyContext::OpenPgp {
889                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
890                            version: nethsm::OpenPgpVersion::V4,
891                        }
892                    )
893                },
894            ],
895        },
896        State {
897            state_type: StateType::NetHsm,
898            users: Vec::new(),
899            keys: Vec::new(),
900        },
901        r#"NetHSM backend error:
902Errors occurred when comparing states:
903Users missing in NetHSM (B), but present in Signstar configuration (A):
904operator1 (role: Operator; tags: tag1)
905admin (role: Administrator)
906Keys missing in NetHSM (B), but present in Signstar configuration (A):
907key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
908"#,
909    )]
910    #[case::differing_users_and_keys(
911        State {
912            state_type: StateType::SignstarConfig,
913            users: vec![
914                UserState{
915                    name: "operator1".parse()?,
916                    role: UserRole::Operator,
917                    tags: vec!["tag1".to_string()]
918                },
919                UserState{
920                    name: "admin".parse()?,
921                    role: UserRole::Administrator,
922                    tags: Vec::new(),
923                },
924            ],
925            keys: vec![
926                KeyState{
927                    name: "key1".parse()?,
928                    namespace: None,
929                    tags: vec!["tag1".to_string()],
930                    key_type: KeyType::Curve25519,
931                    mechanisms: vec![KeyMechanism::EdDsaSignature],
932                    key_cert_state: KeyCertificateState::KeyContext(
933                        CryptographicKeyContext::OpenPgp {
934                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
935                            version: nethsm::OpenPgpVersion::V4,
936                        }
937                    )
938                },
939            ],
940        },
941        State {
942            state_type: StateType::NetHsm,
943            users: vec![
944                UserState{
945                    name: "operator2".parse()?,
946                    role: UserRole::Operator,
947                    tags: vec!["tag2".to_string(), "tag3".to_string()]
948                },
949                UserState{
950                    name: "admin".parse()?,
951                    role: UserRole::Administrator,
952                    tags: Vec::new(),
953                },
954            ],
955            keys: vec![
956                KeyState{
957                    name: "key2".parse()?,
958                    namespace: None,
959                    tags: vec!["tag2".to_string()],
960                    key_type: KeyType::Curve25519,
961                    mechanisms: vec![KeyMechanism::EdDsaSignature],
962                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
963                },
964                KeyState{
965                    name: "key3".parse()?,
966                    namespace: None,
967                    tags: vec!["tag3".to_string()],
968                    key_type: KeyType::Curve25519,
969                    mechanisms: vec![KeyMechanism::EdDsaSignature],
970                    key_cert_state: KeyCertificateState::KeyContext(
971                        CryptographicKeyContext::OpenPgp {
972                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
973                            version: nethsm::OpenPgpVersion::V4,
974                        }
975                    )
976                },
977            ],
978        },
979        r#"NetHSM backend error:
980Errors occurred when comparing states:
981Users missing in NetHSM (B), but present in Signstar configuration (A):
982operator1 (role: Operator; tags: tag1)
983Users missing in Signstar configuration (B), but present in NetHSM (A):
984operator2 (role: Operator; tags: tag2, tag3)
985Keys missing in NetHSM (B), but present in Signstar configuration (A):
986key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
987Keys missing in Signstar configuration (B), but present in NetHSM (A):
988key2 (tags: tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)
989key3 (tags: tag3; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
990"#,
991    )]
992    #[case::user_and_key_mismatch(
993        State {
994            state_type: StateType::SignstarConfig,
995            users: vec![
996                UserState{
997                    name: "operator1".parse()?,
998                    role: UserRole::Operator,
999                    tags: vec!["tag1".to_string()]
1000                },
1001                UserState{
1002                    name: "admin".parse()?,
1003                    role: UserRole::Administrator,
1004                    tags: Vec::new(),
1005                },
1006            ],
1007            keys: vec![
1008                KeyState{
1009                    name: "key1".parse()?,
1010                    namespace: None,
1011                    tags: vec!["tag1".to_string()],
1012                    key_type: KeyType::Curve25519,
1013                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1014                    key_cert_state: KeyCertificateState::KeyContext(
1015                        CryptographicKeyContext::OpenPgp {
1016                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
1017                            version: nethsm::OpenPgpVersion::V4,
1018                        }
1019                    )
1020                },
1021            ],
1022        },
1023        State {
1024            state_type: StateType::NetHsm,
1025            users: vec![
1026                UserState{
1027                    name: "operator1".parse()?,
1028                    role: UserRole::Metrics,
1029                    tags: Vec::new(),
1030                },
1031                UserState{
1032                    name: "admin".parse()?,
1033                    role: UserRole::Administrator,
1034                    tags: Vec::new(),
1035                },
1036            ],
1037            keys: vec![
1038                KeyState{
1039                    name: "key1".parse()?,
1040                    namespace: None,
1041                    tags: vec!["tag1".to_string()],
1042                    key_type: KeyType::Curve25519,
1043                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1044                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
1045                },
1046            ],
1047        },
1048        r#"NetHSM backend error:
1049Errors occurred when comparing states:
1050User mismatch:
1051Signstar configuration (A) => operator1 (role: Operator; tags: tag1)
1052NetHSM (B) => operator1 (role: Metrics)
1053Key mismatch:
1054Signstar configuration (A) => key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
1055NetHSM (B) => key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)
1056"#,
1057    )]
1058    fn state_compare_fails(
1059        #[case] state_a: State,
1060        #[case] state_b: State,
1061        #[case] expected: &str,
1062    ) -> TestResult {
1063        init_logger();
1064
1065        let compare_result = state_a.compare(&state_b);
1066
1067        match compare_result {
1068            Ok(_) => panic!("Comparison should have failed but succeeded"),
1069            Err(error) => assert_eq!(error.to_string(), expected),
1070        }
1071
1072        Ok(())
1073    }
1074}