simple_ldap/
simple_dn.rs

1//! A type representing a simple Distinguished Name.
2//!
3//! E.g. "CN=Tea,OU=Leaves,OU=Are,DC=Great,DC=Org"
4//!
5//! The LDAP spec formally allows you to include almost anything in a DN, but these features are
6//! rarely used. This simple DN representation covers the common cases, and is easy to work with.
7//!
8
9use chumsky::{
10    error::Rich, extra, prelude::{any, just, none_of, one_of},
11    IterParser, Parser
12};
13use itertools::{EitherOrBoth, Itertools};
14use serde_with::{DeserializeFromStr, SerializeDisplay};
15use std::{cmp::Ordering, fmt::Display, str::FromStr};
16use thiserror::Error;
17
18/// LDAP Distinguished Name
19///
20/// Only deals with the common DNs of the form:
21/// "CN=Tea,OU=Leaves,OU=Are,DC=Great,DC=Org"
22///
23/// Multivalued relative DNs and unprintable characters are not supported,
24/// and neither is the empty DN.
25///
26/// ```
27/// use simple_ldap::SimpleDN;
28/// use std::str::FromStr;
29///
30/// // Create a new DN from a string slice
31/// let dn = SimpleDN::from_str("CN=hong,OU=cha,DC=tea").unwrap();
32/// ```
33///
34/// If you do need to handle more exotic DNs, have a look at the crate [`ldap_types`](https://docs.rs/ldap-types/latest/ldap_types/basic/struct.DistinguishedName.html).
35#[derive(Debug, DeserializeFromStr, SerializeDisplay, Clone, PartialEq, Eq)]
36pub struct SimpleDN {
37    /// The relative distinguished names of this DN.
38    /// I.e. the individual key-value pairs.
39    ///
40    /// The ordering is that of the print representation.
41    /// I.e. the leftmost element gets index 0.
42    ///
43    /// **Invariant: This is never empty.**
44    rdns: Vec<SimpleRDN>,
45}
46
47impl Display for SimpleDN {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        // Just interspacing formatted rdns with commas.
50        write!(f, "{}", self.rdns.iter().format(","))
51    }
52}
53
54impl FromStr for SimpleDN {
55    type Err = SimpleDnParseError;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        match simple_dn_parser().parse(s).into_result() {
59            Ok(simple_rdn) => Ok(simple_rdn),
60            Err(rich_errors) => Err(SimpleDnParseError {
61                errors: rich_errors
62                    .into_iter()
63                    // This step gets rid of the lifetime parameters.
64                    .map(|rich_err| ToString::to_string(&rich_err))
65                    .collect(),
66            }),
67        }
68    }
69}
70
71/// Partial ordering is implemented according to DN ancestry.
72/// I.e. A DN being "bigger" than another means that it is the ancestor of the other.
73impl PartialOrd for SimpleDN {
74    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
75        let most_significant_differing_rdn = self
76            .rdns
77            .iter()
78            .rev()
79            .zip_longest(other.rdns.iter().rev())
80            .find(
81                |maybe_both| !matches!(maybe_both, EitherOrBoth::Both(this, that) if this == that),
82            );
83
84        match most_significant_differing_rdn {
85            // There were no differences.
86            None => Some(Ordering::Equal),
87            Some(maybe_both) => match maybe_both {
88                // DNs branch, and aren't comparable.
89                EitherOrBoth::Both(_, _) => None,
90                // DNs are equal, except this one is longer.
91                // Thus this is a child of the other.
92                EitherOrBoth::Left(_) => Some(Ordering::Less),
93                EitherOrBoth::Right(_) => Some(Ordering::Greater),
94            },
95        }
96    }
97}
98
99/// Find the "maximal" common ancestor of two DNs, if any.
100/// Maximal here means that the returned DN is as long as possible.
101pub fn common_ancestor(left: &SimpleDN, right: &SimpleDN) -> Option<SimpleDN> {
102    let mut common_rdns = left.rdns
103        .iter()
104        .rev()
105        .zip(right.rdns.iter().rev())
106        .take_while(|(left, right)| left == right)
107        // Doesn't matter which one we take here as they are the same.
108        .map(|(left, _)| left.clone())
109        .collect_vec();
110
111    // Flip back to correct order.
112    // There would probably be a way to avoid this call, but it's not that expensive.
113    common_rdns.reverse();
114
115    if common_rdns.is_empty() {
116        // No common ancestry at all.
117        None
118    }
119    else {
120        Some(SimpleDN { rdns: common_rdns })
121    }
122}
123
124fn simple_dn_parser<'src>() -> impl Parser<'src, &'src str, SimpleDN, extra::Err<Rich<'src, char>>>
125{
126    simple_rdn_parser()
127        // Just parsing a list of RDNs.
128        .separated_by(just(','))
129        .collect::<Vec<SimpleRDN>>()
130        .map(|rdns| SimpleDN { rdns })
131}
132
133/// Convenience operations for DNs.
134impl SimpleDN {
135    /// Get the value of the first occurrance of the argument RDN key.
136    ///
137    /// E.g. Getting "OU" from "CN=Teas,OU=Are,OU=Really,DC=Awesome" results in "Are".
138    ///
139    /// Probably this only makes sense in keys like "CN" that are expected to be unique.
140    pub fn get(&self, key: &str) -> Option<&str> {
141        self.rdns
142            .iter()
143            .find(|rdn| rdn.key == key)
144            .map(|rdn| rdn.value.as_str())
145    }
146
147    /// Like `get()` but returns all the RDNs starting from the asked key.
148    pub fn get_starting_from(&self, key: &str) -> Option<SimpleDN> {
149        self.rdns
150            .iter()
151            .position(|rdn| rdn.key == key)
152            .map(|position| {
153                let (_, tail) = self.rdns.as_slice().split_at(position);
154
155                SimpleDN {
156                    rdns: tail.to_owned(),
157                }
158            })
159    }
160
161    /// Get the type of this DN.
162    /// The kind of object it denominates.
163    /// I.e. the key of the first RDN.
164    ///
165    /// E.g. the type of "OU=Tea,DC=Drinker" is "OU".
166    ///
167    /// If you want the value too, you can follow this up with `get()`.
168    pub fn get_type(&self) -> &str {
169        #[allow(clippy::expect_used, reason = "Relying on struct invariant.")]
170        &self
171            .rdns
172            .first()
173            .expect("Invariant violation. SimpleDN should never be empty.")
174            .key
175    }
176
177    /// Get the parent DN of this one, if there is one.
178    ///
179    /// E.g. The parent "OU=Puerh,DC=Tea" is "DC=Tea".
180    pub fn parent(&self) -> Option<SimpleDN> {
181        match self.rdns.as_slice() {
182            [_, rest @ ..] if !rest.is_empty() => Some(SimpleDN {
183                rdns: rest.to_owned(),
184            }),
185            _ => None,
186        }
187    }
188}
189
190/// LDAP Relative Distinguished Name
191///
192/// I.e. a single key-value pair like "OU=Matcha" in DN "CN=Whisk,OU=Matcha,DC=Tea".
193///
194/// Only deals with RDN's with a single printable key-value pair.
195///
196/// <https://ldapwiki.com/wiki/Wiki.jsp?page=Relative%20Distinguished%20Name>
197#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
198#[display("{key}={value}")]
199struct SimpleRDN {
200    /// Common examples include: CN, OU, DC
201    ///
202    /// OIDs are not supported here.
203    //  (Though we arent' doing anything to prevent them either.)
204    pub key: String,
205    pub value: String,
206}
207
208/// Parse a single RDN.
209/// This isn't a faithfull reproduction of the LDAP spec,
210/// just dealing with the common case like this:
211///
212/// "CN=Tea Drinker"
213fn simple_rdn_parser<'src>() -> impl Parser<'src, &'src str, SimpleRDN, extra::Err<Rich<'src, char>>>
214{
215    let rdn_key = any()
216        // This probably doesn't quite conform to the spec.
217        .filter(|c: &char| c.is_ascii_alphanumeric())
218        .repeated()
219        .at_least(1)
220        .collect::<String>()
221        // Consume the delimiting equals here too.
222        .then_ignore(just('='));
223
224    // Special characters that must be escaped in DN values:
225    // https://ldapwiki.com/wiki/Wiki.jsp?page=DN%20Escape%20Values
226    //
227    // TODO: Leading and trailing spaces also should be escaped, but they cannot be experessed here.
228    let special = r##",\#+<>;"="##;
229
230    // Escaped special character.
231    // This correctly rejects escaped non-special characters, which is not allowed in LDAP.
232    //
233    // This does not remove the escape characters.
234    // That could be a feature worth investigating, but we would need to implement
235    // value escaping then too.
236    // For now this at least rejects unsound escapes.
237    let escaped = just('\\').then(one_of(special))
238        // This is needed to consolidate the different lengths of "tokens" here.
239        // This parser would output tuples of charts, where as we normally output single chars.
240        .to_slice();
241
242    // Just making sure that this is not a multivalued rdn.
243    // These we don't support.
244    let rdn_value = none_of(special)
245        // Making this char a slice too to make the or() outputs agree.
246        .to_slice()
247        .or(escaped)
248        .repeated()
249        .at_least(1)
250        .to_slice()
251        .map(ToString::to_string);
252
253    // Finally combine the RDN
254    rdn_key
255        .then(rdn_value)
256        .map(|(key, value)| SimpleRDN { key, value })
257}
258
259#[derive(Error, Debug)]
260#[error("Couldn't parse DN: {:?}", self.errors)]
261pub struct SimpleDnParseError {
262    // Have to store these here as strings, because the actuly `Rich`
263    // type has a lifetime parameter, which we don't want to propagate upwards.
264    errors: Vec<String>,
265}
266
267#[cfg(test)]
268mod tests {
269
270    use super::*;
271    use serde::{Deserialize, Serialize};
272
273    static EXAMPLE_DN: &str = "CN=Yabukita,OU=Green,OU=Tea,DC=Japan";
274
275    static EXAMPLE_DN_QUOTED: &str = "\"CN=Yabukita,OU=Green,OU=Tea,DC=Japan\"";
276
277    /// Get a SimpleDN corresponding to `EXAMPLE_DN` above.
278    fn example_simple_dn() -> SimpleDN {
279        SimpleDN {
280            rdns: vec![
281                SimpleRDN {
282                    key: String::from("CN"),
283                    value: String::from("Yabukita"),
284                },
285                SimpleRDN {
286                    key: String::from("OU"),
287                    value: String::from("Green"),
288                },
289                SimpleRDN {
290                    key: String::from("OU"),
291                    value: String::from("Tea"),
292                },
293                SimpleRDN {
294                    key: String::from("DC"),
295                    value: String::from("Japan"),
296                },
297            ],
298        }
299    }
300
301    #[test]
302    fn parse_simple_rdn_ok() {
303        let key = "CN";
304        let value = "Tea Drinker";
305
306        let unstructured = String::new() + key + "=" + value;
307
308        let rdn = simple_rdn_parser()
309            .parse(&unstructured)
310            .into_result()
311            .unwrap();
312
313        assert_eq!(key, rdn.key);
314        assert_eq!(value, rdn.value);
315    }
316
317    #[test]
318    fn parse_simple_rdn_fail() {
319        let key = "CN";
320        let value = "Tea Drinker";
321
322        let unstructured = String::new() + key + "=" + value + "+ANOTHER=5";
323
324        let parse_result = simple_rdn_parser().parse(&unstructured).into_result();
325
326        let errors = parse_result.unwrap_err();
327
328        println!("{errors:#?}");
329    }
330
331    #[test]
332    fn parse_sipmle_dn_ok() {
333        let parsed_dn = simple_dn_parser().parse(EXAMPLE_DN).into_result().unwrap();
334
335        assert_eq!(parsed_dn, example_simple_dn());
336    }
337
338    #[test]
339    fn parse_complex_dn() {
340        "CN=one+OTHER=two,OU=some,DC=thing".parse::<SimpleDN>()
341            .expect_err("Multivalued DN should be rejected.");
342    }
343
344    #[test]
345    fn parse_dn_escapes() -> anyhow::Result<()>{
346        let parsed = SimpleDN::from_str(r"CN=tea \+ milk \= milktea,OU=mixes,DC=odd\,domain")?;
347
348        let expected = SimpleDN { rdns: vec![
349            SimpleRDN {key: String::from("CN"), value: String::from("tea \\+ milk \\= milktea")},
350            SimpleRDN {key: String::from("OU"), value: String::from("mixes")},
351            SimpleRDN {key: String::from("DC"), value: String::from("odd\\,domain")},
352        ]};
353
354        assert_eq!(parsed, expected);
355
356        Ok(())
357    }
358
359    #[test]
360    fn dispaly_simple_dn() {
361        let displayed = example_simple_dn().to_string();
362        assert_eq!(displayed, EXAMPLE_DN);
363    }
364
365    /// For testing serde implementations.
366    #[derive(Debug, Deserialize, Serialize)]
367    #[serde(transparent)]
368    struct DnStruct {
369        pub dn: SimpleDN,
370    }
371
372    impl DnStruct {
373        fn example() -> Self {
374            DnStruct {
375                dn: example_simple_dn(),
376            }
377        }
378    }
379
380    #[test]
381    fn serialize() -> anyhow::Result<()> {
382        let serialized = serde_json::to_string(&DnStruct::example())?;
383        assert_eq!(serialized, EXAMPLE_DN_QUOTED);
384        Ok(())
385    }
386
387    #[test]
388    fn deserialize() -> anyhow::Result<()> {
389        let deserialized: DnStruct = serde_json::from_str(EXAMPLE_DN_QUOTED)?;
390        assert_eq!(deserialized.dn, DnStruct::example().dn);
391        Ok(())
392    }
393
394    #[test]
395    fn get() {
396        let example_dn = example_simple_dn();
397
398        assert_eq!(example_dn.get("OU"), Some("Green"));
399        assert_eq!(example_dn.get("CN"), Some("Yabukita"));
400        assert_eq!(example_dn.get("Nonsense"), None);
401    }
402
403    #[test]
404    fn get_type() {
405        assert_eq!(example_simple_dn().get_type(), "CN");
406    }
407
408    #[test]
409    fn get_parent() {
410        let parent = example_simple_dn().parent();
411        let correct_parent = SimpleDN {
412            rdns: vec![
413                SimpleRDN {
414                    key: String::from("OU"),
415                    value: String::from("Green"),
416                },
417                SimpleRDN {
418                    key: String::from("OU"),
419                    value: String::from("Tea"),
420                },
421                SimpleRDN {
422                    key: String::from("DC"),
423                    value: String::from("Japan"),
424                },
425            ],
426        };
427
428        assert_eq!(parent, Some(correct_parent.clone()));
429
430        let no_parents = SimpleDN {
431            rdns: vec![SimpleRDN {
432                key: String::from("DC"),
433                value: String::from("Tea"),
434            }],
435        };
436
437        assert_eq!(no_parents.parent(), None);
438    }
439
440    #[test]
441    fn get_starting_from() {
442        let example_dn = example_simple_dn();
443
444        let got = example_dn.get_starting_from("OU");
445        let correct = example_dn.parent();
446
447        assert!(got.is_some());
448        assert_eq!(got, correct);
449
450        let non_existent = example_dn.get_starting_from("Coffee");
451        assert_eq!(non_existent, None);
452    }
453
454    #[test]
455    fn get_type_starting_from() {
456        let example_dn = example_simple_dn();
457
458        let dn_type = example_dn.get_type();
459        let starting_from = example_dn.get_starting_from(dn_type);
460
461        // This should always be true.
462        assert_eq!(starting_from, Some(example_dn));
463    }
464
465    #[test]
466    fn partial_compare() {
467        let reflexivity = example_simple_dn().partial_cmp(&example_simple_dn());
468        assert_eq!(reflexivity, Some(Ordering::Equal));
469
470        let great = SimpleDN {
471            rdns: vec![SimpleRDN {
472                key: String::from("DC"),
473                value: String::from("Big"),
474            }],
475        };
476
477        let lesser = SimpleDN {
478            rdns: vec![
479                SimpleRDN {
480                    key: String::from("OU"),
481                    value: String::from("Medium"),
482                },
483                SimpleRDN {
484                    key: String::from("DC"),
485                    value: String::from("Big"),
486                },
487            ],
488        };
489
490        assert_eq!(great.partial_cmp(&lesser), Some(Ordering::Greater));
491        assert_eq!(lesser.partial_cmp(&great), Some(Ordering::Less));
492
493        // To lesser
494        let incomparable = SimpleDN {
495            rdns: vec![
496                SimpleRDN {
497                    key: String::from("OU"),
498                    value: String::from("Else"),
499                },
500                SimpleRDN {
501                    key: String::from("DC"),
502                    value: String::from("Big"),
503                },
504            ],
505        };
506
507        assert!(lesser.partial_cmp(&incomparable).is_none());
508        assert!(incomparable.partial_cmp(&lesser).is_none());
509    }
510
511    #[test]
512    fn test_common_ancestor() -> anyhow::Result<()> {
513        let left = SimpleDN::from_str("CN=puerh,OU=post-fermented,DC=tea")?;
514        let right = SimpleDN::from_str("CN=liu an,OU=post-fermented,DC=tea")?;
515        let correct_ancestor = SimpleDN::from_str("OU=post-fermented,DC=tea")?;
516
517        let found_ancestor = common_ancestor(&left, &right);
518
519        assert_eq!(found_ancestor, Some(correct_ancestor));
520
521        Ok(())
522    }
523}