signstar_config/config/
utils.rs1use 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
19fn 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
41pub(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
68pub(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#[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#[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#[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}