Skip to main content

signstar_config/config/
utils.rs

1//! Utility functions used in the context of the signstar-config config module.
2
3use std::collections::{BTreeSet, HashSet};
4
5use ssh_key::PublicKey;
6
7#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
8use crate::config::{
9    BackendDomainFilter,
10    BackendKeyIdFilter,
11    BackendUserIdFilter,
12    BackendUserIdKind,
13    MappingBackendDomain,
14    MappingBackendKeyId,
15    MappingBackendUserIds,
16};
17use crate::config::{MappingAuthorizedKeyEntry, MappingSystemUserId};
18
19/// Collects all duplicate items from an [`Iterator`] of type `T`.
20fn collect_duplicates<'a, T>(data: impl Iterator<Item = &'a T>) -> Vec<&'a T>
21where
22    T: Eq + std::hash::Hash + Ord + 'a,
23{
24    let duplicates = {
25        let mut seen = HashSet::new();
26        let mut duplicates = HashSet::new();
27
28        for thing in data {
29            if !seen.insert(thing) {
30                duplicates.insert(thing);
31            }
32        }
33        duplicates
34    };
35
36    let mut output = Vec::from_iter(duplicates);
37    output.sort();
38    output
39}
40
41/// Collects all duplicate system user IDs.
42///
43/// Accepts a set of [`MappingSystemUserId`] implementations.
44pub(crate) fn duplicate_system_user_ids(
45    mappings: &BTreeSet<impl MappingSystemUserId>,
46) -> Option<String> {
47    let duplicates = collect_duplicates(
48        mappings
49            .iter()
50            .filter_map(|mapping| mapping.system_user_id()),
51    );
52
53    if duplicates.is_empty() {
54        None
55    } else {
56        Some(format!(
57            "the duplicate system user ID{} {}",
58            if duplicates.len() > 1 { "s" } else { "" },
59            duplicates
60                .iter()
61                .map(|id| format!("\"{id}\""))
62                .collect::<Vec<_>>()
63                .join(", ")
64        ))
65    }
66}
67
68/// Collects all duplicate SSH public keys used in `authorized_keys`.
69///
70/// Accepts a set of [`MappingAuthorizedKeyEntry`] implementations.
71///
72/// # Note
73///
74/// Compares the actual [`KeyData`][ssh_key::public::KeyData] of the underlying [`PublicKey`],
75/// because we are interested in whether there are direct matches and we do not consider a public
76/// key comment an invariant.
77/// The ssh-key upstream derives [`Eq`], [`Hash`], [`Ord`], [`PartialEq`] and [`PartialOrd`] for
78/// [`PublicKey`] which means that public key comments are considered as invariants, even if the
79/// [`KeyData`][ssh_key::public::KeyData] matches!
80pub(crate) fn duplicate_authorized_keys(
81    mappings: &BTreeSet<impl MappingAuthorizedKeyEntry>,
82) -> Option<String> {
83    let all_key_data = mappings
84        .iter()
85        .filter_map(|mapping| mapping.authorized_key_entry())
86        .map(|authorized_key_entry| authorized_key_entry.as_ref().public_key().key_data());
87    let duplicates = collect_duplicates(all_key_data);
88
89    if duplicates.is_empty() {
90        None
91    } else {
92        Some(format!(
93            "the duplicate SSH public key{} {}",
94            if duplicates.len() > 1 { "s" } else { "" },
95            duplicates
96                .into_iter()
97                .map(|key_data| format!("\"{}\"", PublicKey::from(key_data.clone()).to_string()))
98                .collect::<Vec<_>>()
99                .join(", ")
100        ))
101    }
102}
103
104/// Collects all duplicate backend user IDs.
105///
106/// Accepts a set of [`MappingBackendUserIds`] implementations.
107#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
108pub(crate) fn duplicate_backend_user_ids(
109    mappings: &BTreeSet<impl MappingBackendUserIds>,
110) -> Option<String> {
111    let all_backend_user_ids = mappings
112        .iter()
113        .flat_map(|mapping| {
114            mapping.backend_user_ids(BackendUserIdFilter {
115                backend_user_id_kind: BackendUserIdKind::Any,
116            })
117        })
118        .collect::<Vec<_>>();
119    let duplicates = collect_duplicates(all_backend_user_ids.iter());
120
121    if duplicates.is_empty() {
122        None
123    } else {
124        Some(format!(
125            "the duplicate backend user ID{} {}",
126            if duplicates.len() > 1 { "s" } else { "" },
127            duplicates
128                .iter()
129                .map(|id| format!("\"{id}\""))
130                .collect::<Vec<_>>()
131                .join(", ")
132        ))
133    }
134}
135
136/// Collects all duplicate backend key IDs.
137///
138/// Accepts a set of [`MappingBackendKeyId`] implementations.
139/// Allows passing in an implementation of [`BackendKeyIdFilter`] as filter.
140/// Optionally, a `key_type` can be passed in which is used to complete the sentence "the
141/// duplicate{key_type} key ID".
142#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
143pub(crate) fn duplicate_key_ids<T>(
144    mappings: &BTreeSet<impl MappingBackendKeyId<T>>,
145    filter: &T,
146    key_type: Option<String>,
147) -> Option<String>
148where
149    T: BackendKeyIdFilter,
150{
151    let all_system_wide_key_ids = mappings
152        .iter()
153        .filter_map(|mapping| mapping.backend_key_id(filter))
154        .collect::<Vec<_>>();
155    let duplicates = collect_duplicates(all_system_wide_key_ids.iter());
156
157    if duplicates.is_empty() {
158        None
159    } else {
160        Some(format!(
161            "the duplicate{} key ID{} {}",
162            key_type.unwrap_or_default(),
163            if duplicates.len() > 1 { "s" } else { "" },
164            duplicates
165                .iter()
166                .map(|id| format!("\"{id}\""))
167                .collect::<Vec<_>>()
168                .join(", ")
169        ))
170    }
171}
172
173/// Collects all duplicate domains.
174///
175/// Accepts a set of [`MappingBackendDomain`] implementations.
176/// Allows passing in an implementation of [`BackendDomainFilter`] as filter.
177/// Optionally, a `domain_context` and `domain_name` can be passed in which are used to complete the
178/// sentence "the duplicate{domain_context} {domain_name}".
179#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
180pub(crate) fn duplicate_domains<T>(
181    mappings: &BTreeSet<impl MappingBackendDomain<T>>,
182    filter: Option<&T>,
183    domain_context: Option<String>,
184    domain_name: Option<&str>,
185) -> Option<String>
186where
187    T: BackendDomainFilter,
188{
189    let all_domains = mappings
190        .iter()
191        .filter_map(|mapping| mapping.backend_domain(filter))
192        .collect::<Vec<String>>();
193    let duplicates = collect_duplicates(all_domains.iter());
194
195    if duplicates.is_empty() {
196        None
197    } else {
198        Some(format!(
199            "the duplicate{} {}{} {}",
200            domain_context.unwrap_or_default(),
201            domain_name.unwrap_or("domain"),
202            if duplicates.len() > 1 { "s" } else { "" },
203            duplicates
204                .iter()
205                .map(|domain| format!("\"{domain}\""))
206                .collect::<Vec<_>>()
207                .join(", ")
208        ))
209    }
210}