kanidmd_lib/valueset/
ssh.rs

1use crate::be::dbvalue::DbValueTaggedStringV1;
2use crate::prelude::*;
3use crate::schema::SchemaAttribute;
4use crate::utils::trigraph_iter;
5use crate::valueset::{
6    DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
7};
8use kanidm_proto::scim_v1::JsonValue;
9use kanidm_proto::scim_v1::ScimSshPublicKey;
10use sshkey_attest::proto::PublicKey as SshPublicKey;
11use std::collections::btree_map::Entry as BTreeEntry;
12use std::collections::BTreeMap;
13
14#[derive(Debug, Clone)]
15pub struct ValueSetSshKey {
16    map: BTreeMap<String, SshPublicKey>,
17}
18
19impl ValueSetSshKey {
20    pub fn new(t: String, k: SshPublicKey) -> Box<Self> {
21        let mut map = BTreeMap::new();
22        map.insert(t, k);
23        Box::new(ValueSetSshKey { map })
24    }
25
26    pub fn push(&mut self, t: String, k: SshPublicKey) -> bool {
27        self.map.insert(t, k).is_none()
28    }
29
30    pub fn from_dbvs2(data: Vec<DbValueTaggedStringV1>) -> Result<ValueSet, OperationError> {
31        let map = data
32            .into_iter()
33            .filter_map(|DbValueTaggedStringV1 { tag, data }| {
34                SshPublicKey::from_string(&data)
35                    .map_err(|err| {
36                        warn!(%tag, ?err, "discarding corrupted ssh public key");
37                    })
38                    .map(|pk| (tag, pk))
39                    .ok()
40            })
41            .collect();
42        Ok(Box::new(ValueSetSshKey { map }))
43    }
44
45    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
46    // types, and tuples are always foreign.
47    #[allow(clippy::should_implement_trait)]
48    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
49    where
50        T: IntoIterator<Item = (String, SshPublicKey)>,
51    {
52        let map = iter.into_iter().collect();
53        Some(Box::new(ValueSetSshKey { map }))
54    }
55}
56
57impl ValueSetScimPut for ValueSetSshKey {
58    fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
59        let value: Vec<ScimSshPublicKey> = serde_json::from_value(value).map_err(|err| {
60            error!(?err, "SCIM Ssh Public Key syntax invalid");
61            OperationError::SC0024SshPublicKeySyntaxInvalid
62        })?;
63
64        let map = value
65            .into_iter()
66            .map(|ScimSshPublicKey { label, value }| (label, value))
67            .collect();
68
69        Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetSshKey {
70            map,
71        })))
72    }
73}
74
75impl ValueSetT for ValueSetSshKey {
76    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
77        match value {
78            Value::SshKey(t, k) => {
79                if let BTreeEntry::Vacant(e) = self.map.entry(t) {
80                    e.insert(k);
81                    Ok(true)
82                } else {
83                    Ok(false)
84                }
85            }
86            _ => Err(OperationError::InvalidValueState),
87        }
88    }
89
90    fn clear(&mut self) {
91        self.map.clear();
92    }
93
94    fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
95        match pv {
96            PartialValue::SshKey(t) => self.map.remove(t.as_str()).is_some(),
97            _ => false,
98        }
99    }
100
101    fn contains(&self, pv: &PartialValue) -> bool {
102        match pv {
103            PartialValue::SshKey(t) => self.map.contains_key(t.as_str()),
104            _ => false,
105        }
106    }
107
108    fn substring(&self, _pv: &PartialValue) -> bool {
109        false
110    }
111
112    fn startswith(&self, _pv: &PartialValue) -> bool {
113        false
114    }
115
116    fn endswith(&self, _pv: &PartialValue) -> bool {
117        false
118    }
119
120    fn lessthan(&self, _pv: &PartialValue) -> bool {
121        false
122    }
123
124    fn len(&self) -> usize {
125        self.map.len()
126    }
127
128    fn generate_idx_eq_keys(&self) -> Vec<String> {
129        self.map.keys().cloned().collect()
130    }
131
132    fn generate_idx_sub_keys(&self) -> Vec<String> {
133        let lower: Vec<_> = self.map.keys().map(|s| s.to_lowercase()).collect();
134        let mut trigraphs: Vec<_> = lower.iter().flat_map(|v| trigraph_iter(v)).collect();
135
136        trigraphs.sort_unstable();
137        trigraphs.dedup();
138
139        trigraphs.into_iter().map(String::from).collect()
140    }
141
142    fn syntax(&self) -> SyntaxType {
143        SyntaxType::SshKey
144    }
145
146    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
147        self.map.iter().all(|(s, _key)| {
148            Value::validate_str_escapes(s)
149                // && Value::validate_iname(s)
150                && Value::validate_singleline(s)
151        })
152    }
153
154    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
155        Box::new(self.map.iter().map(|(tag, pk)| format!("{}: {}", tag, pk)))
156    }
157
158    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
159        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
160            self.map
161                .iter()
162                .map(|(label, value)| ScimSshPublicKey {
163                    label: label.clone(),
164                    value: value.clone(),
165                })
166                .collect::<Vec<_>>(),
167        )))
168    }
169
170    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
171        DbValueSetV2::SshKey(
172            self.map
173                .iter()
174                .map(|(tag, key)| DbValueTaggedStringV1 {
175                    tag: tag.clone(),
176                    data: key.to_string(),
177                })
178                .collect(),
179        )
180    }
181
182    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
183        Box::new(self.map.keys().cloned().map(PartialValue::SshKey))
184    }
185
186    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
187        Box::new(
188            self.map
189                .iter()
190                .map(|(t, k)| Value::SshKey(t.clone(), k.clone())),
191        )
192    }
193
194    fn equal(&self, other: &ValueSet) -> bool {
195        if let Some(other) = other.as_sshkey_map() {
196            &self.map == other
197        } else {
198            debug_assert!(false);
199            false
200        }
201    }
202
203    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
204        if let Some(b) = other.as_sshkey_map() {
205            mergemaps!(self.map, b)
206        } else {
207            debug_assert!(false);
208            Err(OperationError::InvalidValueState)
209        }
210    }
211
212    fn as_sshkey_map(&self) -> Option<&BTreeMap<String, SshPublicKey>> {
213        Some(&self.map)
214    }
215
216    fn get_ssh_tag(&self, tag: &str) -> Option<&SshPublicKey> {
217        self.map.get(tag)
218    }
219
220    fn as_sshpubkey_string_iter(&self) -> Option<Box<dyn Iterator<Item = String> + '_>> {
221        Some(Box::new(self.map.values().map(|pk| pk.to_string())))
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::{SshPublicKey, ValueSetSshKey};
228    use crate::prelude::ValueSet;
229
230    #[test]
231    fn test_scim_ssh_public_key() {
232        let ecdsa = concat!("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3B",
233        "tOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81l",
234        "zPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@a",
235        "methyst");
236
237        let vs: ValueSet = ValueSetSshKey::new(
238            "label".to_string(),
239            SshPublicKey::from_string(ecdsa).unwrap(),
240        );
241
242        let data = r#"
243[
244  {
245    "label": "label",
246    "value": "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGyIY7o3BtOzRiJ9vvjj96bRImwmyy5GvFSIUPlK00HitiAWGhiO1jGZKmK7220Oe4rqU3uAwA00a0758UODs+0OQHLMDRtl81lzPrVSdrYEDldxH9+a86dBZhdm0e15+ODDts2LHUknsJCRRldO4o9R9VrohlF7cbyBlnhJQrR4S+Oag== william@amethyst"
247  }
248]
249        "#;
250        crate::valueset::scim_json_reflexive(&vs, data);
251
252        // Test that we can parse json values into a valueset.
253        crate::valueset::scim_json_put_reflexive::<ValueSetSshKey>(&vs, &[])
254    }
255}