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