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
7use crate::config::{
8    BackendDomainFilter,
9    BackendKeyIdFilter,
10    BackendUserIdFilter,
11    BackendUserIdKind,
12    MappingAuthorizedKeyEntry,
13    MappingBackendDomain,
14    MappingBackendKeyId,
15    MappingBackendUserIds,
16    MappingSystemUserId,
17};
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.
107pub(crate) fn duplicate_backend_user_ids(
108    mappings: &BTreeSet<impl MappingBackendUserIds>,
109) -> Option<String> {
110    let all_backend_user_ids = mappings
111        .iter()
112        .flat_map(|mapping| {
113            mapping.backend_user_ids(BackendUserIdFilter {
114                backend_user_id_kind: BackendUserIdKind::Any,
115            })
116        })
117        .collect::<Vec<_>>();
118    let duplicates = collect_duplicates(all_backend_user_ids.iter());
119
120    if duplicates.is_empty() {
121        None
122    } else {
123        Some(format!(
124            "the duplicate backend user ID{} {}",
125            if duplicates.len() > 1 { "s" } else { "" },
126            duplicates
127                .iter()
128                .map(|id| format!("\"{id}\""))
129                .collect::<Vec<_>>()
130                .join(", ")
131        ))
132    }
133}
134
135/// Collects all duplicate backend key IDs.
136///
137/// Accepts a set of [`MappingBackendKeyId`] implementations.
138/// Allows passing in an implementation of [`BackendKeyIdFilter`] as filter.
139/// Optionally, a `key_type` can be passed in which is used to complete the sentence "the
140/// duplicate{key_type} key ID".
141pub(crate) fn duplicate_key_ids<T>(
142    mappings: &BTreeSet<impl MappingBackendKeyId<T>>,
143    filter: &T,
144    key_type: Option<String>,
145) -> Option<String>
146where
147    T: BackendKeyIdFilter,
148{
149    let all_system_wide_key_ids = mappings
150        .iter()
151        .filter_map(|mapping| mapping.backend_key_id(filter))
152        .collect::<Vec<_>>();
153    let duplicates = collect_duplicates(all_system_wide_key_ids.iter());
154
155    if duplicates.is_empty() {
156        None
157    } else {
158        Some(format!(
159            "the duplicate{} key ID{} {}",
160            key_type.unwrap_or_default(),
161            if duplicates.len() > 1 { "s" } else { "" },
162            duplicates
163                .iter()
164                .map(|id| format!("\"{id}\""))
165                .collect::<Vec<_>>()
166                .join(", ")
167        ))
168    }
169}
170
171/// Collects all duplicate domains.
172///
173/// Accepts a set of [`MappingBackendDomain`] implementations.
174/// Allows passing in an implementation of [`BackendDomainFilter`] as filter.
175/// Optionally, a `domain_context` and `domain_name` can be passed in which are used to complete the
176/// sentence "the duplicate{domain_context} {domain_name}".
177pub(crate) fn duplicate_domains<T>(
178    mappings: &BTreeSet<impl MappingBackendDomain<T>>,
179    filter: Option<&T>,
180    domain_context: Option<String>,
181    domain_name: Option<&str>,
182) -> Option<String>
183where
184    T: BackendDomainFilter,
185{
186    let all_domains = mappings
187        .iter()
188        .filter_map(|mapping| mapping.backend_domain(filter))
189        .collect::<Vec<String>>();
190    let duplicates = collect_duplicates(all_domains.iter());
191
192    if duplicates.is_empty() {
193        None
194    } else {
195        Some(format!(
196            "the duplicate{} {}{} {}",
197            domain_context.unwrap_or_default(),
198            domain_name.unwrap_or("domain"),
199            if duplicates.len() > 1 { "s" } else { "" },
200            duplicates
201                .iter()
202                .map(|domain| format!("\"{domain}\""))
203                .collect::<Vec<_>>()
204                .join(", ")
205        ))
206    }
207}