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};
23#[cfg(doc)]
24use pgp::composed::SignedPublicKey;
25use strum::IntoStaticStr;
26
27use crate::NetHsmBackendError;
28use crate::{FilterUserKeys, SignstarConfig};
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<&SignstarConfig> for State {
483    /// Creates a [`State`] from [`SignstarConfig`] reference.
484    fn from(value: &SignstarConfig) -> 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;
530    use nethsm::OpenPgpUserIdList;
531    use rstest::rstest;
532    use signstar_common::logging::setup_logging;
533    use testresult::TestResult;
534
535    use super::*;
536
537    /// Ensures that [`UserState::to_string`] shows correctly.
538    #[rstest]
539    #[case(
540        UserState{
541            name: "testuser".parse()?,
542            role: UserRole::Operator,
543            tags: vec!["tag1".to_string(), "tag2".to_string()]
544        },
545        "testuser (role: Operator; tags: tag1, tag2)",
546    )]
547    #[case(
548        UserState{
549            name: "testuser".parse()?,
550            role: UserRole::Operator,
551            tags: Vec::new(),
552        },
553        "testuser (role: Operator)",
554    )]
555    #[case(
556        UserState{
557            name: "testuser".parse()?,
558            role: UserRole::Metrics,
559            tags: Vec::new(),
560        },
561        "testuser (role: Metrics)",
562    )]
563    #[case(
564        UserState{
565            name: "testuser".parse()?,
566            role: UserRole::Backup,
567            tags: Vec::new(),
568        },
569        "testuser (role: Backup)",
570    )]
571    #[case(
572        UserState{name:
573            "testuser".parse()?,
574            role: UserRole::Administrator,
575            tags: Vec::new(),
576        },
577        "testuser (role: Administrator)",
578    )]
579    fn user_state_to_string(#[case] user_state: UserState, #[case] expected: &str) -> TestResult {
580        setup_logging(LevelFilter::Debug)?;
581
582        assert_eq!(user_state.to_string(), expected);
583        Ok(())
584    }
585
586    /// Ensures that [`KeyState::to_string`] shows correctly.
587    #[rstest]
588    #[case::namespaced_key_with_openpgp_v4_cert(
589        KeyState{
590            name: "key1".parse()?,
591            namespace: Some("ns1".parse()?),
592            tags: vec!["tag1".to_string(), "tag2".to_string()],
593            key_type: KeyType::Curve25519,
594            mechanisms: vec![KeyMechanism::EdDsaSignature],
595            key_cert_state: KeyCertificateState::KeyContext(
596                CryptographicKeyContext::OpenPgp {
597                    user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
598                    version: nethsm::OpenPgpVersion::V4,
599                })
600        },
601        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: \"Foobar McFooface <foobar@mcfooface.org>\"))",
602    )]
603    #[case::namespaced_key_with_raw_cert(
604        KeyState{
605            name: "key1".parse()?,
606            namespace: Some("ns1".parse()?),
607            tags: vec!["tag1".to_string(), "tag2".to_string()],
608            key_type: KeyType::Curve25519,
609            mechanisms: vec![KeyMechanism::EdDsaSignature],
610            key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
611        },
612        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
613    )]
614    #[case::namespaced_key_with_no_cert(
615        KeyState{
616            name: "key1".parse()?,
617            namespace: Some("ns1".parse()?),
618            tags: vec!["tag1".to_string(), "tag2".to_string()],
619            key_type: KeyType::Curve25519,
620            mechanisms: vec![KeyMechanism::EdDsaSignature],
621            key_cert_state: KeyCertificateState::Empty
622        },
623        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Empty)",
624    )]
625    #[case::namespaced_key_with_cert_error(
626        KeyState{
627            name: "key1".parse()?,
628            namespace: Some("ns1".parse()?),
629            tags: vec!["tag1".to_string(), "tag2".to_string()],
630            key_type: KeyType::Curve25519,
631            mechanisms: vec![KeyMechanism::EdDsaSignature],
632            key_cert_state: KeyCertificateState::Error { message: "the dog ate it".to_string() }
633        },
634        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Error retrieving key certificate - the dog ate it)",
635    )]
636    #[case::namespaced_key_with_not_a_cert_context(
637        KeyState{
638            name: "key1".parse()?,
639            namespace: Some("ns1".parse()?),
640            tags: vec!["tag1".to_string(), "tag2".to_string()],
641            key_type: KeyType::Curve25519,
642            mechanisms: vec![KeyMechanism::EdDsaSignature],
643            key_cert_state: KeyCertificateState::NotACryptographicKeyContext { message: "failed to convert".to_string() }
644        },
645        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Not a cryptographic key context - \"failed to convert\")",
646    )]
647    #[case::namespaced_key_with_not_an_openpgp_cert(
648        KeyState{
649            name: "key1".parse()?,
650            namespace: Some("ns1".parse()?),
651            tags: vec!["tag1".to_string(), "tag2".to_string()],
652            key_type: KeyType::Curve25519,
653            mechanisms: vec![KeyMechanism::EdDsaSignature],
654            key_cert_state: KeyCertificateState::NotAnOpenPgpCertificate { message: "it's a blob".to_string() }
655        },
656        "key1 (namespace: ns1; tags: tag1, tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Not an OpenPGP certificate - \"it's a blob\")",
657    )]
658    #[case::system_wide_key_with_no_cert_and_no_tags_and_raw_cert(
659        KeyState{
660            name: "key1".parse()?,
661            namespace: None,
662            tags: Vec::new(),
663            key_type: KeyType::Curve25519,
664            mechanisms: vec![KeyMechanism::EdDsaSignature],
665            key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
666        },
667        "key1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
668    )]
669    fn key_state_to_string(#[case] key_state: KeyState, #[case] expected: &str) -> TestResult {
670        setup_logging(LevelFilter::Debug)?;
671
672        assert_eq!(key_state.to_string(), expected);
673        Ok(())
674    }
675
676    /// Ensures that [`StateType::to_string`] shows correctly.
677    #[rstest]
678    #[case(StateType::NetHsm, "NetHSM")]
679    #[case(StateType::SignstarConfig, "Signstar configuration")]
680    fn state_type_display(#[case] state_type: StateType, #[case] expected: &str) -> TestResult {
681        setup_logging(LevelFilter::Debug)?;
682
683        assert_eq!(state_type.to_string(), expected);
684        Ok(())
685    }
686
687    /// Ensures that [`KeyStateType::to_string`] shows correctly.
688    #[rstest]
689    #[case(
690        KeyStateType{
691            state_type: StateType::NetHsm,
692            key_state: KeyState{
693                name: "key1".parse()?,
694                namespace: None,
695                tags: Vec::new(),
696                key_type: KeyType::Curve25519,
697                mechanisms: vec![KeyMechanism::EdDsaSignature],
698                key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
699            },
700        },
701        "NetHSM (key): key1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
702    )]
703    #[case(
704        KeyStateType{
705            state_type: StateType::SignstarConfig,
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        "Signstar configuration (key): key1 (type: Curve25519; mechanisms: EdDsaSignature; context: Raw)",
716    )]
717    fn key_state_type_display(
718        #[case] state_type: KeyStateType,
719        #[case] expected: &str,
720    ) -> TestResult {
721        setup_logging(LevelFilter::Debug)?;
722
723        assert_eq!(state_type.to_string(), expected);
724        Ok(())
725    }
726
727    /// Ensures that [`UserStateType::to_string`] shows correctly.
728    #[rstest]
729    #[case(
730        UserStateType{
731            state_type: StateType::NetHsm,
732            user_state: UserState{
733                name: "testuser".parse()?,
734                role: UserRole::Administrator,
735                tags: Vec::new(),
736            }
737        },
738        "NetHSM (user): testuser (role: Administrator)",
739    )]
740    #[case(
741        UserStateType{
742            state_type: StateType::SignstarConfig,
743            user_state: UserState{
744                name: "testuser".parse()?,
745                role: UserRole::Administrator,
746                tags: Vec::new(),
747            },
748        },
749        "Signstar configuration (user): testuser (role: Administrator)",
750    )]
751    fn user_state_type_display(
752        #[case] state_type: UserStateType,
753        #[case] expected: &str,
754    ) -> TestResult {
755        setup_logging(LevelFilter::Debug)?;
756
757        assert_eq!(state_type.to_string(), expected);
758        Ok(())
759    }
760
761    /// Ensures that [`State::compare`] successfully compares [`State`] containing the same data.
762    #[rstest]
763    #[case::empty(
764        State {
765            state_type: StateType::SignstarConfig,
766            users: Vec::new(),
767            keys: Vec::new(),
768        },
769        State {
770            state_type: StateType::NetHsm,
771            users: Vec::new(),
772            keys: Vec::new(),
773        },
774    )]
775    #[case::with_users_and_keys(
776        State {
777            state_type: StateType::SignstarConfig,
778            users: vec![
779                UserState{
780                    name: "operator1".parse()?,
781                    role: UserRole::Operator,
782                    tags: vec!["tag1".to_string()]
783                },
784                UserState{
785                    name: "admin".parse()?,
786                    role: UserRole::Administrator,
787                    tags: Vec::new(),
788                },
789            ],
790            keys: vec![
791                KeyState{
792                    name: "key1".parse()?,
793                    namespace: None,
794                    tags: vec!["tag1".to_string()],
795                    key_type: KeyType::Curve25519,
796                    mechanisms: vec![KeyMechanism::EdDsaSignature],
797                    key_cert_state: KeyCertificateState::KeyContext(
798                        CryptographicKeyContext::OpenPgp {
799                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
800                            version: nethsm::OpenPgpVersion::V4,
801                        }
802                    )
803                },
804            ],
805        },
806        State {
807            state_type: StateType::NetHsm,
808            users: vec![
809                UserState{
810                    name: "operator1".parse()?,
811                    role: UserRole::Operator,
812                    tags: vec!["tag1".to_string()]
813                },
814                UserState{
815                    name: "admin".parse()?,
816                    role: UserRole::Administrator,
817                    tags: Vec::new(),
818                },
819            ],
820            keys: vec![
821                KeyState{
822                    name: "key1".parse()?,
823                    namespace: None,
824                    tags: vec!["tag1".to_string()],
825                    key_type: KeyType::Curve25519,
826                    mechanisms: vec![KeyMechanism::EdDsaSignature],
827                    key_cert_state: KeyCertificateState::KeyContext(
828                        CryptographicKeyContext::OpenPgp {
829                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
830                            version: nethsm::OpenPgpVersion::V4,
831                        }
832                    )
833                },
834            ],
835        },
836    )]
837    fn state_compare_succeeds(#[case] state_a: State, #[case] state_b: State) -> TestResult {
838        setup_logging(LevelFilter::Debug)?;
839
840        let compare_result = state_a.compare(&state_b);
841
842        if let Err(error) = compare_result {
843            panic!("Comparison should have succeeded but failed:\n{error}")
844        }
845
846        Ok(())
847    }
848
849    /// Ensures that [`State::compare`] fails on [`State`] containing differing data.
850    #[rstest]
851    #[case::one_empty(
852        State {
853            state_type: StateType::SignstarConfig,
854            users: vec![
855                UserState{
856                    name: "operator1".parse()?,
857                    role: UserRole::Operator,
858                    tags: vec!["tag1".to_string()]
859                },
860                UserState{
861                    name: "admin".parse()?,
862                    role: UserRole::Administrator,
863                    tags: Vec::new(),
864                },
865            ],
866            keys: vec![
867                KeyState{
868                    name: "key1".parse()?,
869                    namespace: None,
870                    tags: vec!["tag1".to_string()],
871                    key_type: KeyType::Curve25519,
872                    mechanisms: vec![KeyMechanism::EdDsaSignature],
873                    key_cert_state: KeyCertificateState::KeyContext(
874                        CryptographicKeyContext::OpenPgp {
875                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
876                            version: nethsm::OpenPgpVersion::V4,
877                        }
878                    )
879                },
880            ],
881        },
882        State {
883            state_type: StateType::NetHsm,
884            users: Vec::new(),
885            keys: Vec::new(),
886        },
887        r#"NetHSM backend error:
888Errors occurred when comparing states:
889Users missing in NetHSM (B), but present in Signstar configuration (A):
890operator1 (role: Operator; tags: tag1)
891admin (role: Administrator)
892Keys missing in NetHSM (B), but present in Signstar configuration (A):
893key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
894"#,
895    )]
896    #[case::differing_users_and_keys(
897        State {
898            state_type: StateType::SignstarConfig,
899            users: vec![
900                UserState{
901                    name: "operator1".parse()?,
902                    role: UserRole::Operator,
903                    tags: vec!["tag1".to_string()]
904                },
905                UserState{
906                    name: "admin".parse()?,
907                    role: UserRole::Administrator,
908                    tags: Vec::new(),
909                },
910            ],
911            keys: vec![
912                KeyState{
913                    name: "key1".parse()?,
914                    namespace: None,
915                    tags: vec!["tag1".to_string()],
916                    key_type: KeyType::Curve25519,
917                    mechanisms: vec![KeyMechanism::EdDsaSignature],
918                    key_cert_state: KeyCertificateState::KeyContext(
919                        CryptographicKeyContext::OpenPgp {
920                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
921                            version: nethsm::OpenPgpVersion::V4,
922                        }
923                    )
924                },
925            ],
926        },
927        State {
928            state_type: StateType::NetHsm,
929            users: vec![
930                UserState{
931                    name: "operator2".parse()?,
932                    role: UserRole::Operator,
933                    tags: vec!["tag2".to_string(), "tag3".to_string()]
934                },
935                UserState{
936                    name: "admin".parse()?,
937                    role: UserRole::Administrator,
938                    tags: Vec::new(),
939                },
940            ],
941            keys: vec![
942                KeyState{
943                    name: "key2".parse()?,
944                    namespace: None,
945                    tags: vec!["tag2".to_string()],
946                    key_type: KeyType::Curve25519,
947                    mechanisms: vec![KeyMechanism::EdDsaSignature],
948                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
949                },
950                KeyState{
951                    name: "key3".parse()?,
952                    namespace: None,
953                    tags: vec!["tag3".to_string()],
954                    key_type: KeyType::Curve25519,
955                    mechanisms: vec![KeyMechanism::EdDsaSignature],
956                    key_cert_state: KeyCertificateState::KeyContext(
957                        CryptographicKeyContext::OpenPgp {
958                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
959                            version: nethsm::OpenPgpVersion::V4,
960                        }
961                    )
962                },
963            ],
964        },
965        r#"NetHSM backend error:
966Errors occurred when comparing states:
967Users missing in NetHSM (B), but present in Signstar configuration (A):
968operator1 (role: Operator; tags: tag1)
969Users missing in Signstar configuration (B), but present in NetHSM (A):
970operator2 (role: Operator; tags: tag2, tag3)
971Keys missing in NetHSM (B), but present in Signstar configuration (A):
972key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
973Keys missing in Signstar configuration (B), but present in NetHSM (A):
974key2 (tags: tag2; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)
975key3 (tags: tag3; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
976"#,
977    )]
978    #[case::user_and_key_mismatch(
979        State {
980            state_type: StateType::SignstarConfig,
981            users: vec![
982                UserState{
983                    name: "operator1".parse()?,
984                    role: UserRole::Operator,
985                    tags: vec!["tag1".to_string()]
986                },
987                UserState{
988                    name: "admin".parse()?,
989                    role: UserRole::Administrator,
990                    tags: Vec::new(),
991                },
992            ],
993            keys: vec![
994                KeyState{
995                    name: "key1".parse()?,
996                    namespace: None,
997                    tags: vec!["tag1".to_string()],
998                    key_type: KeyType::Curve25519,
999                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1000                    key_cert_state: KeyCertificateState::KeyContext(
1001                        CryptographicKeyContext::OpenPgp {
1002                            user_ids: OpenPgpUserIdList::new(vec!["Foobar McFooface <foobar@mcfooface.org>".parse()?])?,
1003                            version: nethsm::OpenPgpVersion::V4,
1004                        }
1005                    )
1006                },
1007            ],
1008        },
1009        State {
1010            state_type: StateType::NetHsm,
1011            users: vec![
1012                UserState{
1013                    name: "operator1".parse()?,
1014                    role: UserRole::Metrics,
1015                    tags: Vec::new(),
1016                },
1017                UserState{
1018                    name: "admin".parse()?,
1019                    role: UserRole::Administrator,
1020                    tags: Vec::new(),
1021                },
1022            ],
1023            keys: vec![
1024                KeyState{
1025                    name: "key1".parse()?,
1026                    namespace: None,
1027                    tags: vec!["tag1".to_string()],
1028                    key_type: KeyType::Curve25519,
1029                    mechanisms: vec![KeyMechanism::EdDsaSignature],
1030                    key_cert_state: KeyCertificateState::KeyContext(CryptographicKeyContext::Raw)
1031                },
1032            ],
1033        },
1034        r#"NetHSM backend error:
1035Errors occurred when comparing states:
1036User mismatch:
1037Signstar configuration (A) => operator1 (role: Operator; tags: tag1)
1038NetHSM (B) => operator1 (role: Metrics)
1039Key mismatch:
1040Signstar configuration (A) => key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: OpenPGP (Version: 4; User IDs: "Foobar McFooface <foobar@mcfooface.org>"))
1041NetHSM (B) => key1 (tags: tag1; type: Curve25519; mechanisms: EdDsaSignature; context: Raw)
1042"#,
1043    )]
1044    fn state_compare_fails(
1045        #[case] state_a: State,
1046        #[case] state_b: State,
1047        #[case] expected: &str,
1048    ) -> TestResult {
1049        setup_logging(LevelFilter::Debug)?;
1050
1051        let compare_result = state_a.compare(&state_b);
1052
1053        match compare_result {
1054            Ok(_) => panic!("Comparison should have failed but succeeded"),
1055            Err(error) => assert_eq!(error.to_string(), expected),
1056        }
1057
1058        Ok(())
1059    }
1060}