ldap_types/
schema.rs

1//! Contains all the code related to representing and parsing LDAP schemas
2//!
3//! LDAP Schema is defined in RFC2252 <https://www.rfc-editor.org/rfc/rfc2252.txt>
4
5use std::collections::HashSet;
6
7#[cfg(feature = "chumsky")]
8use chumsky::{prelude::*, text::digits};
9use educe::Educe;
10use enum_as_inner::EnumAsInner;
11use oid::ObjectIdentifier;
12
13use itertools::Itertools;
14
15#[cfg(feature = "chumsky")]
16use lazy_static::lazy_static;
17
18use crate::basic::{KeyString, KeyStringOrOID, OIDWithLength};
19
20#[cfg(feature = "chumsky")]
21use crate::basic::{
22    keystring_or_oid_parser, keystring_parser, oid_parser, quoted_keystring_parser,
23};
24
25#[cfg(feature = "serde")]
26use serde::{Deserialize, Serialize};
27
28/// stores the parameter values that can appear behind a tag in an LDAP schema entry
29#[derive(Clone, Debug, EnumAsInner, Educe)]
30#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31#[educe(PartialEq, Eq, Hash)]
32pub enum LDAPSchemaTagValue {
33    /// the tag has no value
34    Standalone,
35    /// the tag has an OID value
36    OID(#[educe(Hash(method = "crate::basic::hash_oid"))] ObjectIdentifier),
37    /// the tag has an OID value with an optional length
38    OIDWithLength(OIDWithLength),
39    /// the tag has a string value
40    String(String),
41    /// the tag has a key string value
42    KeyString(KeyString),
43    /// the tag has a quoted key string value
44    QuotedKeyString(KeyString),
45    /// the tag has a keystring or an OID value
46    KeyStringOrOID(KeyStringOrOID),
47    /// the tag has a boolean value
48    Boolean(bool),
49    /// the tag has a value that is a list of quoted key strings
50    QuotedKeyStringList(Vec<KeyString>),
51    /// the tag has a value that is a list of key strings or OIDs
52    KeyStringOrOIDList(Vec<KeyStringOrOID>),
53}
54
55/// a single tag in an LDAP schema entry
56#[derive(PartialEq, Eq, Debug, Hash)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58pub struct LDAPSchemaTag {
59    /// the name of the tag
60    tag_name: String,
61    /// the value of the tag, if any
62    tag_value: LDAPSchemaTagValue,
63}
64
65/// encodes the expected value type for a schema tag
66/// this allows code reuse in the parser
67#[cfg(feature = "chumsky")]
68#[derive(PartialEq, Eq, Debug, Hash)]
69#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
70pub enum LDAPSchemaTagType {
71    /// the tag is expected to not have a value
72    Standalone,
73    /// the tag is expected to have an OID value
74    OID,
75    /// the tag is expected to have an OID value with an optional length
76    OIDWithLength,
77    /// the tag is expected to have a string value
78    String,
79    /// the tag is expected to have a key string value
80    KeyString,
81    /// the tag is expected to have a quoted key string value
82    QuotedKeyString,
83    /// the tag is expected to have a key string or an OID value
84    KeyStringOrOID,
85    /// the tag is expected to have a boolean value
86    Boolean,
87    /// the tag is expected to have a value that is a list of quoted key strings
88    QuotedKeyStringList,
89    /// the tag is expected to have a value that is a list of keystrings or OIDs
90    KeyStringOrOIDList,
91}
92
93/// describes an expected tag in an LDAP schema entry
94#[cfg(feature = "chumsky")]
95#[derive(PartialEq, Eq, Debug, Hash)]
96#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
97pub struct LDAPSchemaTagDescriptor {
98    /// the tag name of the expected tag
99    pub tag_name: String,
100    /// the type of parameter we expect the tag to have
101    pub tag_type: LDAPSchemaTagType,
102}
103
104/// this parses the LDAP schema tag value that is described by its parameter
105#[cfg(feature = "chumsky")]
106pub fn ldap_schema_tag_value_parser(
107    tag_type: &LDAPSchemaTagType,
108) -> impl Parser<char, LDAPSchemaTagValue, Error = Simple<char>> {
109    match tag_type {
110        LDAPSchemaTagType::Standalone => empty()
111            .map(|_| LDAPSchemaTagValue::Standalone)
112            .labelled("no value")
113            .boxed(),
114        LDAPSchemaTagType::OID => oid_parser()
115            .map(LDAPSchemaTagValue::OID)
116            .labelled("OID")
117            .boxed(),
118        LDAPSchemaTagType::OIDWithLength => oid_parser()
119            .then(
120                digits(10)
121                    .delimited_by(just('{'), just('}'))
122                    .try_map(|x, span| {
123                        x.parse().map_err(|e| {
124                            Simple::custom(
125                                span,
126                                format!("Failed to convert parsed digits to integer: {}", e),
127                            )
128                        })
129                    })
130                    .or_not(),
131            )
132            .map(|(oid, len)| LDAPSchemaTagValue::OIDWithLength(OIDWithLength { oid, length: len }))
133            .labelled("OID with optional length")
134            .boxed(),
135        LDAPSchemaTagType::String => none_of("'")
136            .repeated()
137            .delimited_by(just('\''), just('\''))
138            .collect::<String>()
139            .map(LDAPSchemaTagValue::String)
140            .labelled("single-quoted string")
141            .boxed(),
142        LDAPSchemaTagType::KeyString => keystring_parser()
143            .map(LDAPSchemaTagValue::KeyString)
144            .labelled("keystring")
145            .boxed(),
146        LDAPSchemaTagType::QuotedKeyString => quoted_keystring_parser()
147            .map(LDAPSchemaTagValue::QuotedKeyString)
148            .labelled("quoted keystring")
149            .boxed(),
150        LDAPSchemaTagType::KeyStringOrOID => keystring_or_oid_parser()
151            .map(LDAPSchemaTagValue::KeyStringOrOID)
152            .labelled("keystring or OID")
153            .boxed(),
154        LDAPSchemaTagType::Boolean => just("TRUE")
155            .to(true)
156            .or(just("FALSE").to(false))
157            .delimited_by(just('\''), just('\''))
158            .map(LDAPSchemaTagValue::Boolean)
159            .labelled("single-quoted uppercase boolean")
160            .boxed(),
161        LDAPSchemaTagType::KeyStringOrOIDList => keystring_or_oid_parser()
162            .padded()
163            .separated_by(just('$'))
164            .delimited_by(just('('), just(')'))
165            .or(keystring_or_oid_parser().map(|x| vec![x]))
166            .map(LDAPSchemaTagValue::KeyStringOrOIDList)
167            .labelled("list of keystrings or OIDs separated by $")
168            .boxed(),
169        LDAPSchemaTagType::QuotedKeyStringList => quoted_keystring_parser()
170            .padded()
171            .repeated()
172            .delimited_by(just('('), just(')'))
173            .or(quoted_keystring_parser().map(|x| vec![x]))
174            .map(LDAPSchemaTagValue::QuotedKeyStringList)
175            .labelled("list of quoted keystrings separated by spaces")
176            .boxed(),
177    }
178}
179
180/// this parses an LDAP schema tag described by its parameter
181#[cfg(feature = "chumsky")]
182pub fn ldap_schema_tag_parser(
183    tag_descriptor: &LDAPSchemaTagDescriptor,
184) -> impl Parser<char, LDAPSchemaTag, Error = Simple<char>> + '_ {
185    just(tag_descriptor.tag_name.to_owned())
186        .padded()
187        .ignore_then(ldap_schema_tag_value_parser(&tag_descriptor.tag_type).padded())
188        .map(move |tag_value| LDAPSchemaTag {
189            tag_name: tag_descriptor.tag_name.to_string(),
190            tag_value,
191        })
192}
193
194/// this parses an LDAP schema entry described by its parameter
195///
196/// the tags can be in any order
197///
198/// this function only parses the tags, it does not check if required tags
199/// exist in the output
200#[cfg(feature = "chumsky")]
201pub fn ldap_schema_parser(
202    tag_descriptors: &[LDAPSchemaTagDescriptor],
203) -> impl Parser<char, (ObjectIdentifier, Vec<LDAPSchemaTag>), Error = Simple<char>> + '_ {
204    let (first, rest) = tag_descriptors
205        .split_first()
206        .expect("tag descriptors must have at least one element");
207    oid_parser()
208        .then(
209            rest.iter()
210                .fold(ldap_schema_tag_parser(first).boxed(), |p, td| {
211                    p.or(ldap_schema_tag_parser(td)).boxed()
212                })
213                .padded()
214                .repeated(),
215        )
216        .padded()
217        .delimited_by(just('('), just(')'))
218}
219
220/// this is used to extract a required tag's value from the result of [ldap_schema_parser]
221#[cfg(feature = "chumsky")]
222pub fn required_tag(
223    tag_name: &str,
224    span: &std::ops::Range<usize>,
225    tags: &[LDAPSchemaTag],
226) -> Result<LDAPSchemaTagValue, Simple<char>> {
227    tags.iter()
228        .find(|x| x.tag_name == tag_name)
229        .ok_or_else(|| {
230            Simple::custom(
231                span.clone(),
232                format!("No {} tag in parsed LDAP schema tag list", tag_name),
233            )
234        })
235        .map(|x| x.tag_value.to_owned())
236}
237
238/// this is used to extract an optional tag's value from the result of [ldap_schema_parser]
239#[cfg(feature = "chumsky")]
240pub fn optional_tag(tag_name: &str, tags: &[LDAPSchemaTag]) -> Option<LDAPSchemaTagValue> {
241    tags.iter()
242        .find(|x| x.tag_name == tag_name)
243        .map(|x| x.tag_value.to_owned())
244}
245
246/// this describes an LDAP syntax schema entry
247#[derive(Clone, Educe)]
248#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
249#[educe(PartialEq, Eq, Hash)]
250pub struct LDAPSyntax {
251    /// the OID of the syntax
252    #[educe(Hash(method = "crate::basic::hash_oid"))]
253    pub oid: ObjectIdentifier,
254    /// the human-readable description of the syntax
255    pub desc: String,
256    /// does this syntax require binary transfer
257    pub x_binary_transfer_required: bool,
258    /// is this syntax human-readable
259    pub x_not_human_readable: bool,
260}
261
262impl std::fmt::Debug for LDAPSyntax {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
264        let string_oid: String = self.oid.clone().into();
265        f.debug_struct("LDAPSyntax")
266            .field("oid", &string_oid)
267            .field("desc", &self.desc)
268            .field(
269                "x_binary_transfer_required",
270                &self.x_binary_transfer_required,
271            )
272            .field("x_not_human_readable", &self.x_not_human_readable)
273            .finish()
274    }
275}
276
277/// parse an LDAP syntax schema entry
278///
279/// <https://ldapwiki.com/wiki/LDAPSyntaxes>
280#[cfg(feature = "chumsky")]
281pub fn ldap_syntax_parser() -> impl Parser<char, LDAPSyntax, Error = Simple<char>> {
282    lazy_static! {
283        static ref LDAP_SYNTAX_TAGS: Vec<LDAPSchemaTagDescriptor> = vec![
284            LDAPSchemaTagDescriptor {
285                tag_name: "DESC".to_string(),
286                tag_type: LDAPSchemaTagType::String
287            },
288            LDAPSchemaTagDescriptor {
289                tag_name: "X-BINARY-TRANSFER-REQUIRED".to_string(),
290                tag_type: LDAPSchemaTagType::Boolean
291            },
292            LDAPSchemaTagDescriptor {
293                tag_name: "X-NOT-HUMAN-READABLE".to_string(),
294                tag_type: LDAPSchemaTagType::Boolean
295            },
296        ];
297    }
298    ldap_schema_parser(&LDAP_SYNTAX_TAGS).try_map(|(oid, tags), span| {
299        Ok(LDAPSyntax {
300            oid,
301            desc: required_tag("DESC", &span, &tags)?
302                .as_string()
303                .unwrap()
304                .to_string(),
305            x_binary_transfer_required: *optional_tag("X-BINARY-TRANSFER-REQUIRED", &tags)
306                .unwrap_or(LDAPSchemaTagValue::Boolean(false))
307                .as_boolean()
308                .unwrap(),
309            x_not_human_readable: *optional_tag("X-NOT-HUMAN-READABLE", &tags)
310                .unwrap_or(LDAPSchemaTagValue::Boolean(false))
311                .as_boolean()
312                .unwrap(),
313        })
314    })
315}
316
317/// a matching rule LDAP schema entry
318///
319/// <https://ldapwiki.com/wiki/MatchingRule>
320#[derive(Clone, Educe)]
321#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
322#[educe(PartialEq, Eq, Hash)]
323pub struct MatchingRule {
324    /// the matching rule's OID
325    #[educe(Hash(method = "crate::basic::hash_oid"))]
326    pub oid: ObjectIdentifier,
327    /// the matching rule's name
328    pub name: Vec<KeyString>,
329    /// the syntax this matching rule can be used with
330    pub syntax: OIDWithLength,
331}
332
333impl std::fmt::Debug for MatchingRule {
334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
335        let string_oid: String = self.oid.clone().into();
336        f.debug_struct("MatchingRule")
337            .field("oid", &string_oid)
338            .field("name", &self.name)
339            .field("syntax", &self.syntax)
340            .finish()
341    }
342}
343
344/// parse a matching rule LDAP schema entry
345#[cfg(feature = "chumsky")]
346pub fn matching_rule_parser() -> impl Parser<char, MatchingRule, Error = Simple<char>> {
347    lazy_static! {
348        static ref MATCHING_RULE_TAGS: Vec<LDAPSchemaTagDescriptor> = vec![
349            LDAPSchemaTagDescriptor {
350                tag_name: "NAME".to_string(),
351                tag_type: LDAPSchemaTagType::QuotedKeyStringList
352            },
353            LDAPSchemaTagDescriptor {
354                tag_name: "SYNTAX".to_string(),
355                tag_type: LDAPSchemaTagType::OIDWithLength
356            },
357        ];
358    }
359    ldap_schema_parser(&MATCHING_RULE_TAGS).try_map(|(oid, tags), span| {
360        Ok(MatchingRule {
361            oid,
362            name: required_tag("NAME", &span, &tags)?
363                .as_quoted_key_string_list()
364                .unwrap()
365                .to_vec(),
366            syntax: required_tag("SYNTAX", &span, &tags)?
367                .as_oid_with_length()
368                .unwrap()
369                .to_owned(),
370        })
371    })
372}
373
374/// parse a matching rule use LDAP schema entry
375///
376/// <https://ldapwiki.com/wiki/MatchingRuleUse>
377#[derive(Clone, Educe)]
378#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
379#[educe(PartialEq, Eq, Hash)]
380pub struct MatchingRuleUse {
381    /// the OID of the matching rule this applies to
382    #[educe(Hash(method = "crate::basic::hash_oid"))]
383    pub oid: ObjectIdentifier,
384    /// the name of the matching rule
385    pub name: Vec<KeyString>,
386    /// the attributes this matching rule can be used with
387    pub applies: Vec<KeyStringOrOID>,
388}
389
390impl std::fmt::Debug for MatchingRuleUse {
391    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
392        let string_oid: String = self.oid.clone().into();
393        f.debug_struct("MatchingRuleUse")
394            .field("oid", &string_oid)
395            .field("name", &self.name)
396            .field("applies", &self.applies)
397            .finish()
398    }
399}
400
401/// parse a matching rule use LDAP schema entry
402#[cfg(feature = "chumsky")]
403pub fn matching_rule_use_parser() -> impl Parser<char, MatchingRuleUse, Error = Simple<char>> {
404    lazy_static! {
405        static ref MATCHING_RULE_USE_TAGS: Vec<LDAPSchemaTagDescriptor> = vec![
406            LDAPSchemaTagDescriptor {
407                tag_name: "NAME".to_string(),
408                tag_type: LDAPSchemaTagType::QuotedKeyStringList
409            },
410            LDAPSchemaTagDescriptor {
411                tag_name: "APPLIES".to_string(),
412                tag_type: LDAPSchemaTagType::KeyStringOrOIDList
413            },
414        ];
415    }
416    ldap_schema_parser(&MATCHING_RULE_USE_TAGS).try_map(|(oid, tags), span| {
417        Ok(MatchingRuleUse {
418            oid,
419            name: required_tag("NAME", &span, &tags)?
420                .as_quoted_key_string_list()
421                .unwrap()
422                .to_vec(),
423            applies: required_tag("APPLIES", &span, &tags)?
424                .as_key_string_or_oid_list()
425                .unwrap()
426                .to_vec(),
427        })
428    })
429}
430
431/// an attribute type LDAP schema entry
432///
433/// <https://ldapwiki.com/wiki/AttributeTypes>
434#[derive(Clone, Educe)]
435#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
436#[educe(PartialEq, Eq, Hash)]
437pub struct AttributeType {
438    /// the OID of the attribute type
439    #[educe(Hash(method = "crate::basic::hash_oid"))]
440    pub oid: ObjectIdentifier,
441    /// the name of the attribute type
442    pub name: Vec<KeyString>,
443    /// the parent in the inheritance tree
444    pub sup: Option<KeyString>,
445    /// a human-readable description
446    pub desc: Option<String>,
447    /// the LDAP syntax of the attribute type
448    pub syntax: Option<OIDWithLength>,
449    /// is this a single or multi-valued attribute
450    pub single_value: bool,
451    /// the equality match to use with this attribute type
452    pub equality: Option<KeyString>,
453    /// the substring match to use with this attribute type
454    pub substr: Option<KeyString>,
455    /// the ordering to use with this attribute type
456    pub ordering: Option<KeyString>,
457    /// is user modification of this attribute type allowed
458    /// (e.g. often operational attributes are not user modifiable)
459    pub no_user_modification: bool,
460    /// if this attribute is a
461    ///
462    /// * user attribute (userApplications)
463    /// * an operational attribute (directoryOperation)
464    /// * an operational attribute that needs to be replicated (distributedOperation)
465    /// * an operational attribute that should not be replicated (dSAOperation)
466    pub usage: Option<KeyString>,
467    /// is this a collective attribute
468    ///
469    /// <https://ldapwiki.com/wiki/Collective%20Attribute>
470    pub collective: bool,
471    /// is this attribute obsolete
472    pub obsolete: bool,
473    /// is this attribute ordered and if so how
474    ///
475    /// * values (order among multiple attribute values is preserved)
476    /// * siblings (order among entries using this attribute as RDN is preserved)
477    ///
478    /// <https://tools.ietf.org/html/draft-chu-ldap-xordered-00>
479    pub x_ordered: Option<KeyString>,
480}
481
482impl std::fmt::Debug for AttributeType {
483    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
484        let string_oid: String = self.oid.clone().into();
485        f.debug_struct("AttributeType")
486            .field("oid", &string_oid)
487            .field("name", &self.name)
488            .field("sup", &self.sup)
489            .field("desc", &self.desc)
490            .field("syntax", &self.syntax)
491            .field("single_value", &self.single_value)
492            .field("equality", &self.equality)
493            .field("substr", &self.substr)
494            .field("ordering", &self.ordering)
495            .field("no_user_modification", &self.no_user_modification)
496            .field("usage", &self.usage)
497            .field("collective", &self.collective)
498            .field("obsolete", &self.obsolete)
499            .field("x_ordered", &self.x_ordered)
500            .finish()
501    }
502}
503
504/// parser for attribute type LDAP schema entries
505#[cfg(feature = "chumsky")]
506pub fn attribute_type_parser() -> impl Parser<char, AttributeType, Error = Simple<char>> {
507    lazy_static! {
508        static ref ATTRIBUTE_TYPE_TAGS: Vec<LDAPSchemaTagDescriptor> = vec![
509            LDAPSchemaTagDescriptor {
510                tag_name: "NAME".to_string(),
511                tag_type: LDAPSchemaTagType::QuotedKeyStringList
512            },
513            LDAPSchemaTagDescriptor {
514                tag_name: "SUP".to_string(),
515                tag_type: LDAPSchemaTagType::KeyString
516            },
517            LDAPSchemaTagDescriptor {
518                tag_name: "DESC".to_string(),
519                tag_type: LDAPSchemaTagType::String
520            },
521            LDAPSchemaTagDescriptor {
522                tag_name: "SYNTAX".to_string(),
523                tag_type: LDAPSchemaTagType::OIDWithLength
524            },
525            LDAPSchemaTagDescriptor {
526                tag_name: "EQUALITY".to_string(),
527                tag_type: LDAPSchemaTagType::KeyString
528            },
529            LDAPSchemaTagDescriptor {
530                tag_name: "SUBSTR".to_string(),
531                tag_type: LDAPSchemaTagType::KeyString
532            },
533            LDAPSchemaTagDescriptor {
534                tag_name: "ORDERING".to_string(),
535                tag_type: LDAPSchemaTagType::KeyString
536            },
537            LDAPSchemaTagDescriptor {
538                tag_name: "SINGLE-VALUE".to_string(),
539                tag_type: LDAPSchemaTagType::Standalone
540            },
541            LDAPSchemaTagDescriptor {
542                tag_name: "NO-USER-MODIFICATION".to_string(),
543                tag_type: LDAPSchemaTagType::Standalone
544            },
545            LDAPSchemaTagDescriptor {
546                tag_name: "USAGE".to_string(),
547                tag_type: LDAPSchemaTagType::KeyString
548            },
549            LDAPSchemaTagDescriptor {
550                tag_name: "COLLECTIVE".to_string(),
551                tag_type: LDAPSchemaTagType::Standalone
552            },
553            LDAPSchemaTagDescriptor {
554                tag_name: "OBSOLETE".to_string(),
555                tag_type: LDAPSchemaTagType::Standalone
556            },
557            LDAPSchemaTagDescriptor {
558                tag_name: "X-ORDERED".to_string(),
559                tag_type: LDAPSchemaTagType::QuotedKeyString
560            },
561        ];
562    }
563    ldap_schema_parser(&ATTRIBUTE_TYPE_TAGS).try_map(|(oid, tags), span| {
564        Ok(AttributeType {
565            oid,
566            name: required_tag("NAME", &span, &tags)?
567                .as_quoted_key_string_list()
568                .unwrap()
569                .to_vec(),
570            sup: optional_tag("SUP", &tags).map(|s| s.as_key_string().unwrap().to_owned()),
571            desc: optional_tag("DESC", &tags).map(|v| v.as_string().unwrap().to_string()),
572            syntax: optional_tag("SYNTAX", &tags)
573                .map(|v| v.as_oid_with_length().unwrap().to_owned()),
574            single_value: optional_tag("SINGLE-VALUE", &tags).is_some(),
575            equality: optional_tag("EQUALITY", &tags)
576                .map(|s| s.as_key_string().unwrap().to_owned()),
577            substr: optional_tag("SUBSTR", &tags).map(|s| s.as_key_string().unwrap().to_owned()),
578            ordering: optional_tag("ORDERING", &tags)
579                .map(|s| s.as_key_string().unwrap().to_owned()),
580            no_user_modification: optional_tag("NO-USER-MODIFICATION", &tags).is_some(),
581            usage: optional_tag("USAGE", &tags).map(|s| s.as_key_string().unwrap().to_owned()),
582            collective: optional_tag("COLLECTIVE", &tags).is_some(),
583            obsolete: optional_tag("OBSOLETE", &tags).is_some(),
584            x_ordered: optional_tag("X-ORDERED", &tags)
585                .map(|s| s.as_quoted_key_string().unwrap().to_owned()),
586        })
587    })
588}
589
590/// type of LDAP object class
591#[derive(PartialEq, Eq, Clone, Debug, EnumAsInner, Hash)]
592#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
593pub enum ObjectClassType {
594    /// this can not be used as an actual object class and is purely used
595    /// as a parent for the other types
596    Abstract,
597    /// this is the main objectclass of an object, other than structural classes
598    /// that are ancestors in the inheritance hierarchy only one of these can be used
599    /// on any given LDAP object
600    Structural,
601    /// these are objectclasses that are added on to the main structural object class
602    /// of an entry
603    Auxiliary,
604}
605
606/// an LDAP schema objectclass entry
607#[derive(Clone, Educe)]
608#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
609#[educe(PartialEq, Eq, Hash)]
610pub struct ObjectClass {
611    /// the OID of the object class
612    #[educe(Hash(method = "crate::basic::hash_oid"))]
613    pub oid: ObjectIdentifier,
614    /// the name of the object class
615    pub name: Vec<KeyString>,
616    /// the parent of the object class
617    pub sup: Vec<KeyStringOrOID>,
618    /// the human-readable description
619    pub desc: Option<String>,
620    /// the type of object class
621    pub object_class_type: ObjectClassType,
622    /// the attributes that must be present on an object with this object class
623    pub must: Vec<KeyStringOrOID>,
624    /// the attributes that may optionally also be present on an object with this
625    /// object class
626    pub may: Vec<KeyStringOrOID>,
627    /// is this object class obsolete
628    pub obsolete: bool,
629}
630
631impl std::fmt::Debug for ObjectClass {
632    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
633        let string_oid: String = self.oid.clone().into();
634        f.debug_struct("ObjectClass")
635            .field("oid", &string_oid)
636            .field("name", &self.name)
637            .field("sup", &self.sup)
638            .field("desc", &self.desc)
639            .field("object_class_type", &self.object_class_type)
640            .field("must", &self.must)
641            .field("may", &self.may)
642            .field("obsolete", &self.obsolete)
643            .finish()
644    }
645}
646
647/// parses an LDAP schema object class entry
648#[cfg(feature = "chumsky")]
649pub fn object_class_parser() -> impl Parser<char, ObjectClass, Error = Simple<char>> {
650    lazy_static! {
651        static ref OBJECT_CLASS_TAGS: Vec<LDAPSchemaTagDescriptor> = vec![
652            LDAPSchemaTagDescriptor {
653                tag_name: "NAME".to_string(),
654                tag_type: LDAPSchemaTagType::QuotedKeyStringList
655            },
656            LDAPSchemaTagDescriptor {
657                tag_name: "SUP".to_string(),
658                tag_type: LDAPSchemaTagType::KeyStringOrOIDList
659            },
660            LDAPSchemaTagDescriptor {
661                tag_name: "DESC".to_string(),
662                tag_type: LDAPSchemaTagType::String
663            },
664            LDAPSchemaTagDescriptor {
665                tag_name: "ABSTRACT".to_string(),
666                tag_type: LDAPSchemaTagType::Standalone
667            },
668            LDAPSchemaTagDescriptor {
669                tag_name: "STRUCTURAL".to_string(),
670                tag_type: LDAPSchemaTagType::Standalone
671            },
672            LDAPSchemaTagDescriptor {
673                tag_name: "AUXILIARY".to_string(),
674                tag_type: LDAPSchemaTagType::Standalone
675            },
676            LDAPSchemaTagDescriptor {
677                tag_name: "MUST".to_string(),
678                tag_type: LDAPSchemaTagType::KeyStringOrOIDList
679            },
680            LDAPSchemaTagDescriptor {
681                tag_name: "MAY".to_string(),
682                tag_type: LDAPSchemaTagType::KeyStringOrOIDList
683            },
684            LDAPSchemaTagDescriptor {
685                tag_name: "OBSOLETE".to_string(),
686                tag_type: LDAPSchemaTagType::Standalone
687            },
688        ];
689    }
690    ldap_schema_parser(&OBJECT_CLASS_TAGS).try_map(|(oid, tags), span| {
691        Ok(ObjectClass {
692            oid,
693            name: required_tag("NAME", &span, &tags)?
694                .as_quoted_key_string_list()
695                .unwrap()
696                .to_vec(),
697            sup: optional_tag("SUP", &tags)
698                .map(|s| s.as_key_string_or_oid_list().unwrap().to_owned())
699                .unwrap_or_default(),
700            desc: optional_tag("DESC", &tags).map(|v| v.as_string().unwrap().to_string()),
701            object_class_type: optional_tag("ABSTRACT", &tags)
702                .map(|_| ObjectClassType::Abstract)
703                .or_else(|| optional_tag("STRUCTURAL", &tags).map(|_| ObjectClassType::Structural))
704                .or_else(|| optional_tag("AUXILIARY", &tags).map(|_| ObjectClassType::Auxiliary))
705                .unwrap_or(ObjectClassType::Structural),
706            must: optional_tag("MUST", &tags)
707                .map(|v| v.as_key_string_or_oid_list().unwrap().to_vec())
708                .unwrap_or_default(),
709            may: optional_tag("MAY", &tags)
710                .map(|v| v.as_key_string_or_oid_list().unwrap().to_vec())
711                .unwrap_or_default(),
712            obsolete: optional_tag("OBSOLETE", &tags).is_some(),
713        })
714    })
715}
716
717/// an entire LDAP schema for an LDAP server
718#[derive(Debug, Clone, Hash)]
719#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
720pub struct LDAPSchema {
721    /// the supported LDAP syntaxes
722    pub ldap_syntaxes: Vec<LDAPSyntax>,
723    /// the supported LDAP matching rules
724    pub matching_rules: Vec<MatchingRule>,
725    /// the allowed uses (attributes) for the LDAP matching rules
726    pub matching_rule_use: Vec<MatchingRuleUse>,
727    /// the supported LDAP attribute types
728    pub attribute_types: Vec<AttributeType>,
729    /// the supported LDAP object classes
730    pub object_classes: Vec<ObjectClass>,
731    // these are not implemented by OpenLDAP to the best of my knowledge
732    // pub name_forms: Vec<String>,
733    // pub dit_content_rules: Vec<String>,
734    // pub dit_structure_rules: Vec<String>,
735}
736
737impl LDAPSchema {
738    /// returns the set of allowed attributes (either must or may) for an ObjectClass and all of its super-classes
739    pub fn allowed_attributes(
740        &self,
741        id: impl TryInto<KeyStringOrOID>,
742    ) -> Option<HashSet<&AttributeType>> {
743        if let Some(object_class) = self.find_object_class(id) {
744            let mut result = HashSet::new();
745            for attribute_name in object_class.must.iter().chain(object_class.may.iter()) {
746                if let Some(attribute) = self.find_attribute_type(attribute_name) {
747                    result.insert(attribute);
748                }
749            }
750            for sup in &object_class.sup {
751                if let Some(allowed_attributes) = self.allowed_attributes(sup) {
752                    result.extend(allowed_attributes);
753                }
754            }
755            Some(result)
756        } else {
757            None
758        }
759    }
760
761    /// returns the set of required attributes (must) for an ObjectClass and all of its super-classes
762    pub fn required_attributes(
763        &self,
764        id: impl TryInto<KeyStringOrOID>,
765    ) -> Option<HashSet<&AttributeType>> {
766        if let Some(object_class) = self.find_object_class(id) {
767            let mut result = HashSet::new();
768            for attribute_name in object_class.must.iter() {
769                if let Some(attribute) = self.find_attribute_type(attribute_name) {
770                    result.insert(attribute);
771                }
772            }
773            for sup in &object_class.sup {
774                if let Some(required_attributes) = self.required_attributes(sup) {
775                    result.extend(required_attributes);
776                }
777            }
778            Some(result)
779        } else {
780            None
781        }
782    }
783
784    /// return the object class if it is present in the schema
785    pub fn find_object_class(&self, id: impl TryInto<KeyStringOrOID>) -> Option<&ObjectClass> {
786        let id: Result<KeyStringOrOID, _> = id.try_into();
787        match id {
788            Ok(id) => {
789                let match_fn: Box<dyn FnMut(&&ObjectClass) -> bool> = match id {
790                    KeyStringOrOID::OID(oid) => Box::new(move |at: &&ObjectClass| at.oid == oid),
791                    KeyStringOrOID::KeyString(s) => Box::new(move |at: &&ObjectClass| {
792                        at.name
793                            .iter()
794                            .map(|n| n.to_lowercase())
795                            .contains(&s.to_lowercase())
796                    }),
797                };
798                self.object_classes.iter().find(match_fn)
799            }
800            Err(_) => None,
801        }
802    }
803
804    /// apply the given function to the named object class
805    /// and all its ancestors in the LDAP schema until one
806    /// returns Some
807    pub fn find_object_class_property<'a, R>(
808        &'a self,
809        id: impl TryInto<KeyStringOrOID>,
810        f: fn(&'a ObjectClass) -> Option<&'a R>,
811    ) -> Option<&'a R> {
812        let object_class = self.find_object_class(id);
813        if let Some(object_class) = object_class {
814            if let Some(r) = f(object_class) {
815                Some(r)
816            } else {
817                let ks_or_oids = &object_class.sup;
818                for ks_or_oid in ks_or_oids {
819                    if let Some(r) = self.find_object_class_property(ks_or_oid, f) {
820                        return Some(r);
821                    }
822                }
823                None
824            }
825        } else {
826            None
827        }
828    }
829
830    /// return the attribute type if it is present in the schema
831    pub fn find_attribute_type(&self, id: impl TryInto<KeyStringOrOID>) -> Option<&AttributeType> {
832        let id: Result<KeyStringOrOID, _> = id.try_into();
833        match id {
834            Ok(id) => {
835                let match_fn: Box<dyn FnMut(&&AttributeType) -> bool> = match id {
836                    KeyStringOrOID::OID(oid) => Box::new(move |at: &&AttributeType| at.oid == oid),
837                    KeyStringOrOID::KeyString(s) => Box::new(move |at: &&AttributeType| {
838                        at.name
839                            .iter()
840                            .map(|n| n.to_lowercase())
841                            .contains(&s.to_lowercase())
842                    }),
843                };
844                self.attribute_types.iter().find(match_fn)
845            }
846            Err(_) => None,
847        }
848    }
849
850    /// apply the given function to the named attribute type
851    /// and all its ancestors in the LDAP schema until one
852    /// returns Some
853    pub fn find_attribute_type_property<'a, R>(
854        &'a self,
855        id: impl TryInto<KeyStringOrOID>,
856        f: fn(&'a AttributeType) -> Option<&'a R>,
857    ) -> Option<&'a R> {
858        let attribute_type = self.find_attribute_type(id);
859        if let Some(attribute_type) = attribute_type {
860            if let Some(r) = f(attribute_type) {
861                Some(r)
862            } else if let Some(sup @ KeyString(_)) = &attribute_type.sup {
863                self.find_attribute_type_property(KeyStringOrOID::KeyString(sup.to_owned()), f)
864            } else {
865                None
866            }
867        } else {
868            None
869        }
870    }
871
872    /// return the ldap syntax if it is present in the schema
873    #[cfg(feature = "chumsky")]
874    pub fn find_ldap_syntax(&self, id: impl TryInto<ObjectIdentifier>) -> Option<&LDAPSyntax> {
875        let id: Result<ObjectIdentifier, _> = id.try_into();
876        match id {
877            Ok(id) => self
878                .ldap_syntaxes
879                .iter()
880                .find(move |ls: &&LDAPSyntax| ls.oid == id),
881            Err(_) => None,
882        }
883    }
884
885    /// return the matching rule if it is present in the schema
886    #[cfg(feature = "chumsky")]
887    pub fn find_matching_rule(&self, id: impl TryInto<ObjectIdentifier>) -> Option<&MatchingRule> {
888        let id: Result<ObjectIdentifier, _> = id.try_into();
889        match id {
890            Ok(id) => self
891                .matching_rules
892                .iter()
893                .find(move |ls: &&MatchingRule| ls.oid == id),
894            Err(_) => None,
895        }
896    }
897
898    /// return the matching rule use if it is present in the schema
899    #[cfg(feature = "chumsky")]
900    pub fn find_matching_rule_use(
901        &self,
902        id: impl TryInto<ObjectIdentifier>,
903    ) -> Option<&MatchingRuleUse> {
904        let id: Result<ObjectIdentifier, _> = id.try_into();
905        match id {
906            Ok(id) => self
907                .matching_rule_use
908                .iter()
909                .find(move |ls: &&MatchingRuleUse| ls.oid == id),
910            Err(_) => None,
911        }
912    }
913}
914
915#[cfg(test)]
916mod test {
917    #[cfg(feature = "chumsky")]
918    use super::*;
919
920    #[cfg(feature = "chumsky")]
921    #[test]
922    fn test_parse_ldap_syntax() {
923        assert!(ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )").is_ok());
924    }
925
926    #[cfg(feature = "chumsky")]
927    #[test]
928    fn test_parse_ldap_syntax_value1() {
929        assert_eq!(ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' X-NOT-HUMAN-READABLE 'TRUE' )"),
930            Ok(LDAPSyntax { oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
931                         desc: "Certificate".to_string(),
932                         x_binary_transfer_required: true,
933                         x_not_human_readable: true,
934                       }
935            ));
936    }
937
938    #[cfg(feature = "chumsky")]
939    #[test]
940    fn test_parse_ldap_syntax_value2() {
941        assert_eq!(ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-NOT-HUMAN-READABLE 'TRUE' X-BINARY-TRANSFER-REQUIRED 'TRUE' )"),
942            Ok(LDAPSyntax { oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
943                         desc: "Certificate".to_string(),
944                         x_binary_transfer_required: true,
945                         x_not_human_readable: true,
946                       }
947            ));
948    }
949
950    #[cfg(feature = "chumsky")]
951    #[test]
952    fn test_parse_ldap_syntax_value3() {
953        assert_eq!(ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-BINARY-TRANSFER-REQUIRED 'TRUE' )"),
954            Ok(LDAPSyntax { oid: "1.3.6.1.4.1.1466.115.121.1.8".to_string().try_into().unwrap(),
955                         desc: "Certificate".to_string(),
956                         x_binary_transfer_required: true,
957                         x_not_human_readable: false,
958                       }
959            ));
960    }
961
962    #[cfg(feature = "chumsky")]
963    #[test]
964    fn test_parse_ldap_syntax_value4() {
965        assert_eq!(
966            ldap_syntax_parser().parse(
967                "( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' X-NOT-HUMAN-READABLE 'TRUE' )"
968            ),
969            Ok(LDAPSyntax {
970                oid: "1.3.6.1.4.1.1466.115.121.1.8"
971                    .to_string()
972                    .try_into()
973                    .unwrap(),
974                desc: "Certificate".to_string(),
975                x_binary_transfer_required: false,
976                x_not_human_readable: true,
977            })
978        );
979    }
980
981    #[cfg(feature = "chumsky")]
982    #[test]
983    fn test_parse_ldap_syntax_value5() {
984        assert_eq!(
985            ldap_syntax_parser().parse("( 1.3.6.1.4.1.1466.115.121.1.8 DESC 'Certificate' )"),
986            Ok(LDAPSyntax {
987                oid: "1.3.6.1.4.1.1466.115.121.1.8"
988                    .to_string()
989                    .try_into()
990                    .unwrap(),
991                desc: "Certificate".to_string(),
992                x_binary_transfer_required: false,
993                x_not_human_readable: false,
994            })
995        );
996    }
997
998    #[cfg(feature = "chumsky")]
999    #[test]
1000    fn test_parse_ldap_syntax_desc_required() {
1001        assert!(ldap_syntax_parser()
1002            .parse("( 1.3.6.1.4.1.1466.115.121.1.8 )")
1003            .is_err());
1004    }
1005
1006    #[cfg(feature = "chumsky")]
1007    #[test]
1008    fn test_parse_matching_rule() {
1009        assert!(matching_rule_parser()
1010            .parse("( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )")
1011            .is_ok());
1012    }
1013
1014    #[cfg(feature = "chumsky")]
1015    #[test]
1016    fn test_parse_matching_rule_value() {
1017        assert_eq!(
1018            matching_rule_parser()
1019                .parse("( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )"),
1020            Ok(MatchingRule {
1021                oid: "1.3.6.1.1.16.3".to_string().try_into().unwrap(),
1022                name: vec![KeyString("UUIDOrderingMatch".to_string())],
1023                syntax: OIDWithLength {
1024                    oid: "1.3.6.1.1.16.1".to_string().try_into().unwrap(),
1025                    length: None
1026                },
1027            })
1028        );
1029    }
1030
1031    #[cfg(feature = "chumsky")]
1032    #[test]
1033    fn test_parse_matching_rule_uses() {
1034        assert!(matching_rule_use_parser().parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )").is_ok());
1035    }
1036
1037    #[cfg(feature = "chumsky")]
1038    #[test]
1039    fn test_parse_matching_rule_uses_value() {
1040        assert_eq!(matching_rule_use_parser().parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES ( postalAddress $ registeredAddress $ homePostalAddress ) )"),
1041            Ok(MatchingRuleUse { oid: "2.5.13.11".to_string().try_into().unwrap(),
1042                                 name: vec![KeyString("caseIgnoreListMatch".to_string())],
1043                                 applies: vec![KeyStringOrOID::KeyString(KeyString("postalAddress".to_string())),
1044                                               KeyStringOrOID::KeyString(KeyString("registeredAddress".to_string())),
1045                                               KeyStringOrOID::KeyString(KeyString("homePostalAddress".to_string()))
1046                                              ],
1047            })
1048        );
1049    }
1050
1051    #[cfg(feature = "chumsky")]
1052    #[test]
1053    fn test_parse_matching_rule_uses_single_applies_value() {
1054        assert_eq!(
1055            matching_rule_use_parser()
1056                .parse("( 2.5.13.11 NAME 'caseIgnoreListMatch' APPLIES postalAddress )"),
1057            Ok(MatchingRuleUse {
1058                oid: "2.5.13.11".to_string().try_into().unwrap(),
1059                name: vec![KeyString("caseIgnoreListMatch".to_string())],
1060                applies: vec![KeyStringOrOID::KeyString(KeyString(
1061                    "postalAddress".to_string()
1062                ))],
1063            })
1064        );
1065    }
1066}