Skip to main content

rustlavel_ldap/
protocol.rs

1//! LDAP v3 messages, as RFC 4511 defines them.
2//!
3//! Every exchange is an `LDAPMessage`: a message id, one protocol operation,
4//! and optional controls. The id is how a client matches an answer to a
5//! question — LDAP allows several operations to be in flight on one connection,
6//! so nothing about a response says which request it belongs to except that
7//! number.
8//!
9//! The operations here are the ones an application actually needs to
10//! authenticate somebody: bind, search, unbind, and the StartTLS extended
11//! request. Modify, add and delete are deliberately absent — this package
12//! exists to answer "is this the right password", and a directory is somebody
13//! else's system of record.
14//!
15//! ## Tags
16//!
17//! RFC 4511's ASN.1 module is `IMPLICIT TAGS`, so a context tag *replaces* the
18//! underlying universal one rather than wrapping it: `and [0] SET OF Filter`
19//! goes on the wire as `0xa0`, not as `0xa0` around a `0x31`. The one exception
20//! is `not [2] Filter`, because a tag on a CHOICE is always explicit — so a
21//! `not` filter's contents are a complete inner filter element. Both cases are
22//! encoded and tested below, because getting this backwards produces bytes a
23//! directory rejects with a protocolError and no useful explanation.
24
25use crate::ber::{self, Decoder, Encoder};
26use rustlavel_core::{Error, Result};
27
28/// The protocol version this client speaks. LDAP v2 is not implemented, and
29/// should not be: it has no StartTLS and no UTF-8 guarantee.
30pub const VERSION: i64 = 3;
31
32/// The OID of the StartTLS extended operation (RFC 4511 §4.14.1).
33pub const START_TLS_OID: &str = "1.3.6.1.4.1.1466.20037";
34
35/// `[APPLICATION n]` tags for the protocol operations, from RFC 4511 §4.
36pub mod tags {
37    use crate::ber::application;
38
39    pub const BIND_REQUEST: u8 = application(0, true);
40    pub const BIND_RESPONSE: u8 = application(1, true);
41    /// `UnbindRequest ::= [APPLICATION 2] NULL` — primitive, and always empty.
42    pub const UNBIND_REQUEST: u8 = application(2, false);
43    pub const SEARCH_REQUEST: u8 = application(3, true);
44    pub const SEARCH_RESULT_ENTRY: u8 = application(4, true);
45    pub const SEARCH_RESULT_DONE: u8 = application(5, true);
46    pub const SEARCH_RESULT_REFERENCE: u8 = application(19, true);
47    pub const EXTENDED_REQUEST: u8 = application(23, true);
48    pub const EXTENDED_RESPONSE: u8 = application(24, true);
49    pub const INTERMEDIATE_RESPONSE: u8 = application(25, true);
50}
51
52// ---------------------------------------------------------------------------
53// Result codes
54// ---------------------------------------------------------------------------
55
56/// Every result code RFC 4511 names, plus whatever else a directory sends.
57///
58/// This is an enum rather than a bare integer because the difference between
59/// 32 and 49 is the difference between "no such user" and "wrong password", and
60/// a caller that has to remember which number is which will eventually get it
61/// wrong in the direction that logs somebody in.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ResultCode {
64    Success,
65    OperationsError,
66    ProtocolError,
67    TimeLimitExceeded,
68    SizeLimitExceeded,
69    CompareFalse,
70    CompareTrue,
71    AuthMethodNotSupported,
72    StrongerAuthRequired,
73    Referral,
74    AdminLimitExceeded,
75    UnavailableCriticalExtension,
76    ConfidentialityRequired,
77    SaslBindInProgress,
78    NoSuchAttribute,
79    UndefinedAttributeType,
80    InappropriateMatching,
81    ConstraintViolation,
82    AttributeOrValueExists,
83    InvalidAttributeSyntax,
84    NoSuchObject,
85    AliasProblem,
86    InvalidDnSyntax,
87    AliasDereferencingProblem,
88    InappropriateAuthentication,
89    InvalidCredentials,
90    InsufficientAccessRights,
91    Busy,
92    Unavailable,
93    UnwillingToPerform,
94    LoopDetect,
95    NamingViolation,
96    ObjectClassViolation,
97    NotAllowedOnNonLeaf,
98    NotAllowedOnRdn,
99    EntryAlreadyExists,
100    ObjectClassModsProhibited,
101    AffectsMultipleDsas,
102    Other,
103    /// A code this client does not know. Directories add their own; refusing to
104    /// carry the number would lose the only clue in the message.
105    Unknown(i64),
106}
107
108impl ResultCode {
109    pub fn from_i64(code: i64) -> ResultCode {
110        use ResultCode::*;
111        match code {
112            0 => Success,
113            1 => OperationsError,
114            2 => ProtocolError,
115            3 => TimeLimitExceeded,
116            4 => SizeLimitExceeded,
117            5 => CompareFalse,
118            6 => CompareTrue,
119            7 => AuthMethodNotSupported,
120            8 => StrongerAuthRequired,
121            10 => Referral,
122            11 => AdminLimitExceeded,
123            12 => UnavailableCriticalExtension,
124            13 => ConfidentialityRequired,
125            14 => SaslBindInProgress,
126            16 => NoSuchAttribute,
127            17 => UndefinedAttributeType,
128            18 => InappropriateMatching,
129            19 => ConstraintViolation,
130            20 => AttributeOrValueExists,
131            21 => InvalidAttributeSyntax,
132            32 => NoSuchObject,
133            33 => AliasProblem,
134            34 => InvalidDnSyntax,
135            36 => AliasDereferencingProblem,
136            48 => InappropriateAuthentication,
137            49 => InvalidCredentials,
138            50 => InsufficientAccessRights,
139            51 => Busy,
140            52 => Unavailable,
141            53 => UnwillingToPerform,
142            54 => LoopDetect,
143            64 => NamingViolation,
144            65 => ObjectClassViolation,
145            66 => NotAllowedOnNonLeaf,
146            67 => NotAllowedOnRdn,
147            68 => EntryAlreadyExists,
148            69 => ObjectClassModsProhibited,
149            71 => AffectsMultipleDsas,
150            80 => Other,
151            other => Unknown(other),
152        }
153    }
154
155    pub fn as_i64(self) -> i64 {
156        use ResultCode::*;
157        match self {
158            Success => 0,
159            OperationsError => 1,
160            ProtocolError => 2,
161            TimeLimitExceeded => 3,
162            SizeLimitExceeded => 4,
163            CompareFalse => 5,
164            CompareTrue => 6,
165            AuthMethodNotSupported => 7,
166            StrongerAuthRequired => 8,
167            Referral => 10,
168            AdminLimitExceeded => 11,
169            UnavailableCriticalExtension => 12,
170            ConfidentialityRequired => 13,
171            SaslBindInProgress => 14,
172            NoSuchAttribute => 16,
173            UndefinedAttributeType => 17,
174            InappropriateMatching => 18,
175            ConstraintViolation => 19,
176            AttributeOrValueExists => 20,
177            InvalidAttributeSyntax => 21,
178            NoSuchObject => 32,
179            AliasProblem => 33,
180            InvalidDnSyntax => 34,
181            AliasDereferencingProblem => 36,
182            InappropriateAuthentication => 48,
183            InvalidCredentials => 49,
184            InsufficientAccessRights => 50,
185            Busy => 51,
186            Unavailable => 52,
187            UnwillingToPerform => 53,
188            LoopDetect => 54,
189            NamingViolation => 64,
190            ObjectClassViolation => 65,
191            NotAllowedOnNonLeaf => 66,
192            NotAllowedOnRdn => 67,
193            EntryAlreadyExists => 68,
194            ObjectClassModsProhibited => 69,
195            AffectsMultipleDsas => 71,
196            Other => 80,
197            Unknown(code) => code,
198        }
199    }
200
201    /// The name RFC 4511 gives the code, in the spelling the RFC uses.
202    pub fn name(self) -> &'static str {
203        use ResultCode::*;
204        match self {
205            Success => "success",
206            OperationsError => "operationsError",
207            ProtocolError => "protocolError",
208            TimeLimitExceeded => "timeLimitExceeded",
209            SizeLimitExceeded => "sizeLimitExceeded",
210            CompareFalse => "compareFalse",
211            CompareTrue => "compareTrue",
212            AuthMethodNotSupported => "authMethodNotSupported",
213            StrongerAuthRequired => "strongerAuthRequired",
214            Referral => "referral",
215            AdminLimitExceeded => "adminLimitExceeded",
216            UnavailableCriticalExtension => "unavailableCriticalExtension",
217            ConfidentialityRequired => "confidentialityRequired",
218            SaslBindInProgress => "saslBindInProgress",
219            NoSuchAttribute => "noSuchAttribute",
220            UndefinedAttributeType => "undefinedAttributeType",
221            InappropriateMatching => "inappropriateMatching",
222            ConstraintViolation => "constraintViolation",
223            AttributeOrValueExists => "attributeOrValueExists",
224            InvalidAttributeSyntax => "invalidAttributeSyntax",
225            NoSuchObject => "noSuchObject",
226            AliasProblem => "aliasProblem",
227            InvalidDnSyntax => "invalidDNSyntax",
228            AliasDereferencingProblem => "aliasDereferencingProblem",
229            InappropriateAuthentication => "inappropriateAuthentication",
230            InvalidCredentials => "invalidCredentials",
231            InsufficientAccessRights => "insufficientAccessRights",
232            Busy => "busy",
233            Unavailable => "unavailable",
234            UnwillingToPerform => "unwillingToPerform",
235            LoopDetect => "loopDetect",
236            NamingViolation => "namingViolation",
237            ObjectClassViolation => "objectClassViolation",
238            NotAllowedOnNonLeaf => "notAllowedOnNonLeaf",
239            NotAllowedOnRdn => "notAllowedOnRDN",
240            EntryAlreadyExists => "entryAlreadyExists",
241            ObjectClassModsProhibited => "objectClassModsProhibited",
242            AffectsMultipleDsas => "affectsMultipleDSAs",
243            Other => "other",
244            Unknown(_) => "an unrecognised result code",
245        }
246    }
247
248    pub fn is_success(self) -> bool {
249        matches!(self, ResultCode::Success)
250    }
251}
252
253impl std::fmt::Display for ResultCode {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        write!(f, "{} ({})", self.name(), self.as_i64())
256    }
257}
258
259/// `LDAPResult` — the shape every operation's answer ends with.
260#[derive(Debug, Clone)]
261pub struct LdapResult {
262    pub code: ResultCode,
263    /// How much of the requested DN the directory did find. Empty most of the
264    /// time; useful when a base DN has a typo in it.
265    pub matched_dn: String,
266    /// The directory's own words. Active Directory hides its real reason in
267    /// here as a hex sub-code, which [`crate::directory`] reads.
268    pub diagnostic: String,
269    pub referrals: Vec<String>,
270}
271
272impl LdapResult {
273    pub fn is_success(&self) -> bool {
274        self.code.is_success()
275    }
276
277    /// Turn anything but success into an error that says what failed and why.
278    pub fn into_result(self, operation: &str) -> Result<LdapResult> {
279        if self.is_success() {
280            return Ok(self);
281        }
282
283        let mut message = format!("the directory refused the {operation}: {}", self.code);
284        if !self.diagnostic.is_empty() {
285            message.push_str(&format!(" — {}", self.diagnostic));
286        }
287        if !self.matched_dn.is_empty() {
288            message.push_str(&format!(" (it did find `{}`)", self.matched_dn));
289        }
290        Err(Error::msg(message))
291    }
292
293    /// `COMPONENTS OF LDAPResult`: four fields inlined into the operation's own
294    /// SEQUENCE, not a nested one. Reading them as a nested SEQUENCE is the
295    /// single easiest mistake to make here.
296    fn decode(decoder: &mut Decoder<'_>) -> Result<LdapResult> {
297        let code = ResultCode::from_i64(decoder.enumerated()?);
298        let matched_dn = decoder.string()?;
299        let diagnostic = decoder.string()?;
300
301        let mut referrals = Vec::new();
302        if decoder.has_tag(ber::context(3, true)) {
303            let mut list = decoder.nested(ber::context(3, true))?;
304            while !list.is_empty() {
305                referrals.push(list.string()?);
306            }
307        }
308
309        Ok(LdapResult { code, matched_dn, diagnostic, referrals })
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Controls
315// ---------------------------------------------------------------------------
316
317/// A request or response control (RFC 4511 §4.1.11).
318///
319/// The value stays as raw octets: every control defines its own encoding, and
320/// this package has no business guessing at one it was not told about.
321#[derive(Debug, Clone)]
322pub struct Control {
323    pub oid: String,
324    /// When true, a directory that does not implement the control must refuse
325    /// the whole operation rather than quietly ignoring it.
326    pub critical: bool,
327    pub value: Option<Vec<u8>>,
328}
329
330impl Control {
331    pub fn new(oid: impl Into<String>) -> Control {
332        Control { oid: oid.into(), critical: false, value: None }
333    }
334
335    pub fn critical(mut self, critical: bool) -> Control {
336        self.critical = critical;
337        self
338    }
339
340    pub fn value(mut self, value: impl Into<Vec<u8>>) -> Control {
341        self.value = Some(value.into());
342        self
343    }
344
345    fn encode(&self, encoder: &mut Encoder) {
346        encoder.sequence(|body| {
347            body.string(&self.oid);
348            // DEFAULT FALSE: omitted when false, because a directory comparing
349            // encodings expects the default to be absent.
350            if self.critical {
351                body.boolean(true);
352            }
353            if let Some(value) = &self.value {
354                body.octet_string(value);
355            }
356        });
357    }
358
359    fn decode(decoder: &mut Decoder<'_>) -> Result<Control> {
360        let mut fields = decoder.sequence()?;
361        let oid = fields.string()?;
362        let mut critical = false;
363        let mut value = None;
364
365        if fields.has_tag(ber::BOOLEAN) {
366            critical = fields.boolean()?;
367        }
368        if fields.has_tag(ber::OCTET_STRING) {
369            value = Some(fields.octet_string()?.to_vec());
370        }
371
372        Ok(Control { oid, critical, value })
373    }
374}
375
376// ---------------------------------------------------------------------------
377// Search
378// ---------------------------------------------------------------------------
379
380/// How far below the base DN a search reaches.
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
382pub enum Scope {
383    /// The base entry itself and nothing else.
384    Base,
385    /// The base's immediate children, but not the base.
386    OneLevel,
387    /// The base and everything under it. The default, and what a user lookup
388    /// almost always wants: people are rarely all at one depth.
389    #[default]
390    Subtree,
391}
392
393impl Scope {
394    pub fn as_i64(self) -> i64 {
395        match self {
396            Scope::Base => 0,
397            Scope::OneLevel => 1,
398            Scope::Subtree => 2,
399        }
400    }
401}
402
403/// What to do with alias entries encountered during a search.
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
405pub enum DerefAliases {
406    /// Follow nothing. The default here, and deliberately not the protocol's
407    /// most permissive option: an alias is an indirection somebody else
408    /// controls, and following one during an authentication lookup means the
409    /// entry you bind against need not be the entry you searched for.
410    #[default]
411    Never,
412    InSearching,
413    FindingBaseObject,
414    Always,
415}
416
417impl DerefAliases {
418    pub fn as_i64(self) -> i64 {
419        match self {
420            DerefAliases::Never => 0,
421            DerefAliases::InSearching => 1,
422            DerefAliases::FindingBaseObject => 2,
423            DerefAliases::Always => 3,
424        }
425    }
426}
427
428/// A `SearchRequest`, with the defaults a lookup usually wants already set.
429#[derive(Debug, Clone)]
430pub struct SearchRequest {
431    pub base: String,
432    pub scope: Scope,
433    pub deref: DerefAliases,
434    /// Zero means "no client-side limit"; the directory still has its own.
435    pub size_limit: i64,
436    /// Seconds. Zero means no client-side limit.
437    pub time_limit: i64,
438    /// Return attribute names without their values.
439    pub types_only: bool,
440    pub filter: Filter,
441    /// Empty asks for all user attributes. To ask for none, use the OID `1.1`,
442    /// which RFC 4511 reserves to mean exactly that.
443    pub attributes: Vec<String>,
444}
445
446impl SearchRequest {
447    pub fn new(base: impl Into<String>, filter: Filter) -> SearchRequest {
448        SearchRequest {
449            base: base.into(),
450            scope: Scope::default(),
451            deref: DerefAliases::default(),
452            size_limit: 0,
453            time_limit: 0,
454            types_only: false,
455            filter,
456            attributes: Vec::new(),
457        }
458    }
459
460    pub fn scope(mut self, scope: Scope) -> SearchRequest {
461        self.scope = scope;
462        self
463    }
464
465    pub fn deref(mut self, deref: DerefAliases) -> SearchRequest {
466        self.deref = deref;
467        self
468    }
469
470    pub fn size_limit(mut self, limit: i64) -> SearchRequest {
471        self.size_limit = limit;
472        self
473    }
474
475    pub fn time_limit(mut self, seconds: i64) -> SearchRequest {
476        self.time_limit = seconds;
477        self
478    }
479
480    pub fn types_only(mut self, types_only: bool) -> SearchRequest {
481        self.types_only = types_only;
482        self
483    }
484
485    pub fn attributes<S: Into<String>>(
486        mut self,
487        attributes: impl IntoIterator<Item = S>,
488    ) -> SearchRequest {
489        self.attributes = attributes.into_iter().map(Into::into).collect();
490        self
491    }
492
493    fn encode(&self, encoder: &mut Encoder) {
494        encoder.constructed(tags::SEARCH_REQUEST, |body| {
495            body.string(&self.base);
496            body.enumerated(self.scope.as_i64());
497            body.enumerated(self.deref.as_i64());
498            body.integer(self.size_limit);
499            body.integer(self.time_limit);
500            body.boolean(self.types_only);
501            self.filter.encode(body);
502            body.sequence(|list| {
503                for attribute in &self.attributes {
504                    list.string(attribute);
505                }
506            });
507        });
508    }
509}
510
511/// One attribute of an entry, with every value the directory returned.
512///
513/// Values are octets, not strings. `userCertificate` and `objectGUID` are not
514/// text, and a type that pretends otherwise loses them.
515#[derive(Debug, Clone)]
516pub struct Attribute {
517    pub name: String,
518    pub values: Vec<Vec<u8>>,
519}
520
521impl Attribute {
522    /// The first value as text, if it is text.
523    pub fn first(&self) -> Option<&str> {
524        self.values.first().and_then(|value| std::str::from_utf8(value).ok())
525    }
526}
527
528/// A `SearchResultEntry`: one matching entry and its attributes.
529#[derive(Debug, Clone)]
530pub struct SearchEntry {
531    pub dn: String,
532    pub attributes: Vec<Attribute>,
533}
534
535impl SearchEntry {
536    /// Look an attribute up by name, case-insensitively.
537    ///
538    /// Attribute descriptions are case-insensitive in LDAP, and directories
539    /// disagree about which case they return: OpenLDAP echoes what the schema
540    /// says, Active Directory echoes what you asked for. Matching exactly is a
541    /// bug that only shows up against the other server.
542    pub fn attribute(&self, name: &str) -> Option<&Attribute> {
543        self.attributes.iter().find(|attribute| attribute.name.eq_ignore_ascii_case(name))
544    }
545
546    /// The first value of an attribute, as text.
547    pub fn value(&self, name: &str) -> Option<&str> {
548        self.attribute(name).and_then(Attribute::first)
549    }
550
551    /// Every value of an attribute that is valid text.
552    pub fn values(&self, name: &str) -> Vec<&str> {
553        self.attribute(name)
554            .map(|attribute| {
555                attribute
556                    .values
557                    .iter()
558                    .filter_map(|value| std::str::from_utf8(value).ok())
559                    .collect()
560            })
561            .unwrap_or_default()
562    }
563
564    fn decode(decoder: &mut Decoder<'_>) -> Result<SearchEntry> {
565        let dn = decoder.string()?;
566        let mut attributes = Vec::new();
567
568        let mut list = decoder.sequence()?;
569        while !list.is_empty() {
570            let mut partial = list.sequence()?;
571            let name = partial.string()?;
572            let mut values = Vec::new();
573            let mut set = partial.set()?;
574            while !set.is_empty() {
575                values.push(set.octet_string()?.to_vec());
576            }
577            attributes.push(Attribute { name, values });
578        }
579
580        Ok(SearchEntry { dn, attributes })
581    }
582}
583
584/// Everything one search produced.
585#[derive(Debug, Clone)]
586pub struct SearchOutcome {
587    pub entries: Vec<SearchEntry>,
588    /// Continuation references. This client does not chase them: following a
589    /// referral means binding somewhere the caller never named, with
590    /// credentials the caller never agreed to send there.
591    pub referrals: Vec<Vec<String>>,
592    pub result: LdapResult,
593}
594
595// ---------------------------------------------------------------------------
596// Filters
597// ---------------------------------------------------------------------------
598
599/// A search filter, built as a value rather than pasted together as a string.
600///
601/// This is the security-relevant type in the package. The classic LDAP
602/// injection is a filter template — `(&(objectClass=person)(uid=NAME))` — with
603/// a username interpolated into it: a username of `*` turns the lookup into a
604/// wildcard that matches every account, and one containing `)(uid=admin` re-
605/// writes the filter outright.
606///
607/// A `Filter` cannot be injected into, because it never becomes a string on the
608/// way to the directory. [`Filter::encode`] writes each assertion value as a
609/// length-delimited BER octet string, where `*` and `)` are just bytes with no
610/// syntax of their own.
611///
612/// [`Display`](std::fmt::Display) does produce RFC 4515 text — for logs, and
613/// for anything that genuinely needs the string form — and escapes every value
614/// on the way out.
615#[derive(Debug, Clone, PartialEq, Eq)]
616pub enum Filter {
617    /// `(&(a)(b))`. An empty `And` is the absolute-true filter `(&)` of
618    /// RFC 4526, which is well defined and matches everything.
619    And(Vec<Filter>),
620    /// `(|(a)(b))`. An empty `Or` is RFC 4526's absolute-false filter `(|)`.
621    Or(Vec<Filter>),
622    Not(Box<Filter>),
623    /// `(attribute=value)`
624    Equality { attribute: String, value: String },
625    /// `(attribute>=value)`
626    GreaterOrEqual { attribute: String, value: String },
627    /// `(attribute<=value)`
628    LessOrEqual { attribute: String, value: String },
629    /// `(attribute~=value)`
630    Approximate { attribute: String, value: String },
631    /// `(attribute=*)` — the entry has this attribute at all.
632    Present { attribute: String },
633    /// `(attribute=initial*any*any*last)`. RFC 4511 calls the trailing part
634    /// `final`, which is a Rust keyword, so it is `last` here.
635    Substrings {
636        attribute: String,
637        initial: Option<String>,
638        any: Vec<String>,
639        last: Option<String>,
640    },
641}
642
643impl Filter {
644    pub fn equals(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
645        Filter::Equality { attribute: attribute.into(), value: value.into() }
646    }
647
648    pub fn greater_or_equal(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
649        Filter::GreaterOrEqual { attribute: attribute.into(), value: value.into() }
650    }
651
652    pub fn less_or_equal(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
653        Filter::LessOrEqual { attribute: attribute.into(), value: value.into() }
654    }
655
656    pub fn approximately(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
657        Filter::Approximate { attribute: attribute.into(), value: value.into() }
658    }
659
660    pub fn present(attribute: impl Into<String>) -> Filter {
661        Filter::Present { attribute: attribute.into() }
662    }
663
664    pub fn starts_with(attribute: impl Into<String>, prefix: impl Into<String>) -> Filter {
665        Filter::Substrings {
666            attribute: attribute.into(),
667            initial: Some(prefix.into()),
668            any: Vec::new(),
669            last: None,
670        }
671    }
672
673    pub fn ends_with(attribute: impl Into<String>, suffix: impl Into<String>) -> Filter {
674        Filter::Substrings {
675            attribute: attribute.into(),
676            initial: None,
677            any: Vec::new(),
678            last: Some(suffix.into()),
679        }
680    }
681
682    pub fn contains(attribute: impl Into<String>, fragment: impl Into<String>) -> Filter {
683        Filter::Substrings {
684            attribute: attribute.into(),
685            initial: None,
686            any: vec![fragment.into()],
687            last: None,
688        }
689    }
690
691    pub fn and(filters: impl IntoIterator<Item = Filter>) -> Filter {
692        Filter::And(filters.into_iter().collect())
693    }
694
695    pub fn or(filters: impl IntoIterator<Item = Filter>) -> Filter {
696        Filter::Or(filters.into_iter().collect())
697    }
698
699    /// `(!(…))`. Also spelled `!filter`, through [`std::ops::Not`]; this form
700    /// exists because it chains, and a filter is usually built left to right.
701    pub fn negate(self) -> Filter {
702        Filter::Not(Box::new(self))
703    }
704
705    /// Write the filter as BER, per RFC 4511 §4.5.1.
706    pub fn encode(&self, encoder: &mut Encoder) {
707        match self {
708            Filter::And(filters) => {
709                encoder.constructed(ber::context(0, true), |body| {
710                    for filter in filters {
711                        filter.encode(body);
712                    }
713                });
714            }
715            Filter::Or(filters) => {
716                encoder.constructed(ber::context(1, true), |body| {
717                    for filter in filters {
718                        filter.encode(body);
719                    }
720                });
721            }
722            // [2] wraps a CHOICE, and a tag on a CHOICE is always explicit — so
723            // the contents here are a whole inner filter, tag and all.
724            Filter::Not(inner) => {
725                encoder.constructed(ber::context(2, true), |body| inner.encode(body));
726            }
727            Filter::Equality { attribute, value } => {
728                assertion(encoder, ber::context(3, true), attribute, value);
729            }
730            Filter::Substrings { attribute, initial, any, last } => {
731                encoder.constructed(ber::context(4, true), |body| {
732                    body.string(attribute);
733                    body.sequence(|parts| {
734                        if let Some(initial) = initial {
735                            parts.tagged_string(ber::context(0, false), initial);
736                        }
737                        for fragment in any {
738                            parts.tagged_string(ber::context(1, false), fragment);
739                        }
740                        if let Some(last) = last {
741                            parts.tagged_string(ber::context(2, false), last);
742                        }
743                    });
744                });
745            }
746            Filter::GreaterOrEqual { attribute, value } => {
747                assertion(encoder, ber::context(5, true), attribute, value);
748            }
749            Filter::LessOrEqual { attribute, value } => {
750                assertion(encoder, ber::context(6, true), attribute, value);
751            }
752            // [7] is primitive: the attribute description *is* the contents.
753            Filter::Present { attribute } => {
754                encoder.tagged_string(ber::context(7, false), attribute);
755            }
756            Filter::Approximate { attribute, value } => {
757                assertion(encoder, ber::context(8, true), attribute, value);
758            }
759        }
760    }
761}
762
763/// `!filter`, for the times that reads better than [`Filter::negate`].
764impl std::ops::Not for Filter {
765    type Output = Filter;
766
767    fn not(self) -> Filter {
768        Filter::Not(Box::new(self))
769    }
770}
771
772fn assertion(encoder: &mut Encoder, tag: u8, attribute: &str, value: &str) {
773    encoder.constructed(tag, |body| {
774        body.string(attribute);
775        body.string(value);
776    });
777}
778
779impl std::fmt::Display for Filter {
780    /// The RFC 4515 string form, with every value escaped.
781    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
782        match self {
783            Filter::And(filters) => {
784                f.write_str("(&")?;
785                for filter in filters {
786                    write!(f, "{filter}")?;
787                }
788                f.write_str(")")
789            }
790            Filter::Or(filters) => {
791                f.write_str("(|")?;
792                for filter in filters {
793                    write!(f, "{filter}")?;
794                }
795                f.write_str(")")
796            }
797            Filter::Not(inner) => write!(f, "(!{inner})"),
798            Filter::Equality { attribute, value } => {
799                write!(f, "({}={})", escape_filter_value(attribute), escape_filter_value(value))
800            }
801            Filter::GreaterOrEqual { attribute, value } => {
802                write!(f, "({}>={})", escape_filter_value(attribute), escape_filter_value(value))
803            }
804            Filter::LessOrEqual { attribute, value } => {
805                write!(f, "({}<={})", escape_filter_value(attribute), escape_filter_value(value))
806            }
807            Filter::Approximate { attribute, value } => {
808                write!(f, "({}~={})", escape_filter_value(attribute), escape_filter_value(value))
809            }
810            Filter::Present { attribute } => write!(f, "({}=*)", escape_filter_value(attribute)),
811            Filter::Substrings { attribute, initial, any, last } => {
812                write!(f, "({}=", escape_filter_value(attribute))?;
813                if let Some(initial) = initial {
814                    f.write_str(&escape_filter_value(initial))?;
815                }
816                f.write_str("*")?;
817                for fragment in any {
818                    f.write_str(&escape_filter_value(fragment))?;
819                    f.write_str("*")?;
820                }
821                if let Some(last) = last {
822                    f.write_str(&escape_filter_value(last))?;
823                }
824                f.write_str(")")
825            }
826        }
827    }
828}
829
830/// Escape a value for the RFC 4515 string form of a filter.
831///
832/// RFC 4515 §3 requires five characters to be escaped as `\` plus two hex
833/// digits: the backslash itself, the asterisk, both parentheses, and NUL. The
834/// asterisk is the one that matters. A username of `*` pasted unescaped into
835/// `(uid=NAME)` produces `(uid=*)`, which matches every account in the
836/// directory — and a lookup that returns the first of them is a lookup that has
837/// just chosen somebody else's account for the person logging in.
838///
839/// Use this whenever a value has to reach a filter through text. Values that go
840/// through [`Filter`] and [`Filter::encode`] need no escaping at all, because
841/// BER's octet strings carry their own length and have no metacharacters — that
842/// is the reason the typed builder exists.
843pub fn escape_filter_value(value: &str) -> String {
844    let mut escaped = String::with_capacity(value.len());
845    for character in value.chars() {
846        match character {
847            '\\' => escaped.push_str("\\5c"),
848            '*' => escaped.push_str("\\2a"),
849            '(' => escaped.push_str("\\28"),
850            ')' => escaped.push_str("\\29"),
851            '\0' => escaped.push_str("\\00"),
852            other => escaped.push(other),
853        }
854    }
855    escaped
856}
857
858/// Escape a value for use inside a distinguished name (RFC 4514 §2.4).
859///
860/// The set is different from the filter one, and so is the risk: a DN is what a
861/// bind is *for*, so a username smuggled into `uid=NAME,ou=people,dc=example`
862/// with a comma in it binds against an entry the template never described.
863///
864/// A leading `#` or a leading or trailing space also have to be escaped, and
865/// this handles those with a backslash before the character rather than a hex
866/// escape, which RFC 4514 allows and which stays readable in a log.
867pub fn escape_dn_value(value: &str) -> String {
868    let mut escaped = String::with_capacity(value.len());
869    let characters: Vec<char> = value.chars().collect();
870
871    for (index, &character) in characters.iter().enumerate() {
872        let first = index == 0;
873        let last = index + 1 == characters.len();
874
875        match character {
876            '\0' => escaped.push_str("\\00"),
877            '"' | '+' | ',' | ';' | '<' | '>' | '\\' | '=' => {
878                escaped.push('\\');
879                escaped.push(character);
880            }
881            '#' if first => escaped.push_str("\\#"),
882            ' ' if first || last => escaped.push_str("\\ "),
883            other => escaped.push(other),
884        }
885    }
886
887    escaped
888}
889
890// ---------------------------------------------------------------------------
891// Outgoing operations
892// ---------------------------------------------------------------------------
893
894/// One encoded protocol operation, waiting for a message id.
895///
896/// **No `Debug`.** A `simple_bind` operation's bytes contain the password in
897/// the clear — that is what a simple bind *is* — and a struct that can be
898/// printed is a struct that eventually gets printed into a log.
899pub struct Operation {
900    kind: &'static str,
901    op: Vec<u8>,
902    controls: Vec<Control>,
903    /// Whether sending this puts a password on the wire. The connection checks
904    /// it before writing anything, so the transport rule cannot be forgotten at
905    /// a call site.
906    carries_password: bool,
907}
908
909impl Operation {
910    /// A simple bind: a DN and a password, both in the clear.
911    ///
912    /// # An empty password is an anonymous bind
913    ///
914    /// RFC 4511 §4.2 is explicit: a simple bind with a zero-length password is
915    /// an *unauthenticated* bind, and a directory answers it with `success`. So
916    /// a login form whose password field was left empty, passed straight
917    /// through, comes back as "authenticated" — for any username that exists.
918    /// This has been a real vulnerability in real products more than once.
919    ///
920    /// So it is refused here, at the point the bytes are built, rather than
921    /// anywhere it could be skipped. A caller that genuinely wants an anonymous
922    /// bind asks for one by name with [`Operation::anonymous_bind`].
923    pub fn simple_bind(dn: &str, password: &str) -> Result<Operation> {
924        if password.is_empty() {
925            return Err(Error::msg(
926                "refusing to send a simple bind with an empty password. LDAP treats that as an \
927                 anonymous bind and answers `success`, so a blank password field would report a \
928                 successful login. If an anonymous bind is what you meant, ask for one by name \
929                 with `bind_anonymous`.",
930            ));
931        }
932
933        Ok(Operation {
934            kind: "bind",
935            op: bind_bytes(dn, password),
936            controls: Vec::new(),
937            carries_password: true,
938        })
939    }
940
941    /// An anonymous bind: no name, no password, no authentication.
942    ///
943    /// A legitimate operation — many directories allow an anonymous search —
944    /// and spelled out here so that it can never be reached by accident.
945    pub fn anonymous_bind() -> Operation {
946        Operation {
947            kind: "anonymous bind",
948            op: bind_bytes("", ""),
949            controls: Vec::new(),
950            carries_password: false,
951        }
952    }
953
954    pub fn search(request: &SearchRequest) -> Operation {
955        let mut encoder = Encoder::new();
956        request.encode(&mut encoder);
957        Operation {
958            kind: "search",
959            op: encoder.into_bytes(),
960            controls: Vec::new(),
961            carries_password: false,
962        }
963    }
964
965    /// `UnbindRequest ::= [APPLICATION 2] NULL`. There is no unbind *response*:
966    /// it means "I am closing this connection", not "please reply".
967    pub fn unbind() -> Operation {
968        let mut encoder = Encoder::new();
969        encoder.element(tags::UNBIND_REQUEST, &[]);
970        Operation {
971            kind: "unbind",
972            op: encoder.into_bytes(),
973            controls: Vec::new(),
974            carries_password: false,
975        }
976    }
977
978    pub fn extended(oid: &str, value: Option<&[u8]>) -> Operation {
979        let mut encoder = Encoder::new();
980        encoder.constructed(tags::EXTENDED_REQUEST, |body| {
981            body.tagged_string(ber::context(0, false), oid);
982            if let Some(value) = value {
983                body.element(ber::context(1, false), value);
984            }
985        });
986        Operation {
987            kind: "extended request",
988            op: encoder.into_bytes(),
989            controls: Vec::new(),
990            carries_password: false,
991        }
992    }
993
994    pub fn start_tls() -> Operation {
995        let mut operation = Operation::extended(START_TLS_OID, None);
996        operation.kind = "StartTLS request";
997        operation
998    }
999
1000    pub fn with_control(mut self, control: Control) -> Operation {
1001        self.controls.push(control);
1002        self
1003    }
1004
1005    pub fn kind(&self) -> &'static str {
1006        self.kind
1007    }
1008
1009    pub fn carries_password(&self) -> bool {
1010        self.carries_password
1011    }
1012
1013    /// Wrap the operation in an `LDAPMessage` with the given id.
1014    pub fn encode(&self, message_id: i64) -> Vec<u8> {
1015        let mut encoder = Encoder::new();
1016        encoder.sequence(|body| {
1017            body.integer(message_id);
1018            body.raw(&self.op);
1019            if !self.controls.is_empty() {
1020                body.constructed(ber::context(0, true), |list| {
1021                    for control in &self.controls {
1022                        control.encode(list);
1023                    }
1024                });
1025            }
1026        });
1027        encoder.into_bytes()
1028    }
1029}
1030
1031fn bind_bytes(dn: &str, password: &str) -> Vec<u8> {
1032    let mut encoder = Encoder::new();
1033    encoder.constructed(tags::BIND_REQUEST, |body| {
1034        body.integer(VERSION);
1035        body.string(dn);
1036        // AuthenticationChoice ::= CHOICE { simple [0] OCTET STRING, ... }
1037        body.tagged_string(ber::context(0, false), password);
1038    });
1039    encoder.into_bytes()
1040}
1041
1042// ---------------------------------------------------------------------------
1043// Incoming messages
1044// ---------------------------------------------------------------------------
1045
1046/// A decoded protocol operation, in the shapes this client asks for.
1047#[derive(Debug, Clone)]
1048pub enum ProtocolOp {
1049    BindResponse { result: LdapResult, server_sasl_credentials: Option<Vec<u8>> },
1050    SearchResultEntry(SearchEntry),
1051    SearchResultReference(Vec<String>),
1052    SearchResultDone(LdapResult),
1053    ExtendedResponse { result: LdapResult, name: Option<String>, value: Option<Vec<u8>> },
1054    IntermediateResponse,
1055    /// Something this client never asked for. The tag is kept so the error can
1056    /// name it; the contents are dropped, because guessing at the shape of a
1057    /// message you did not request is how a parser gets surprised.
1058    Unrecognised { tag: u8 },
1059}
1060
1061impl ProtocolOp {
1062    /// The `LDAPResult` inside, for the operations that carry one.
1063    pub fn result(&self) -> Option<&LdapResult> {
1064        match self {
1065            ProtocolOp::BindResponse { result, .. }
1066            | ProtocolOp::SearchResultDone(result)
1067            | ProtocolOp::ExtendedResponse { result, .. } => Some(result),
1068            _ => None,
1069        }
1070    }
1071
1072    pub fn name(&self) -> &'static str {
1073        match self {
1074            ProtocolOp::BindResponse { .. } => "a bind response",
1075            ProtocolOp::SearchResultEntry(_) => "a search result entry",
1076            ProtocolOp::SearchResultReference(_) => "a search result reference",
1077            ProtocolOp::SearchResultDone(_) => "a search result",
1078            ProtocolOp::ExtendedResponse { .. } => "an extended response",
1079            ProtocolOp::IntermediateResponse => "an intermediate response",
1080            ProtocolOp::Unrecognised { .. } => "an operation this client does not implement",
1081        }
1082    }
1083}
1084
1085/// One `LDAPMessage` off the wire.
1086#[derive(Debug, Clone)]
1087pub struct LdapMessage {
1088    /// Zero means an unsolicited notification (RFC 4511 §4.4) — the directory
1089    /// speaking without being asked, which in practice means it is about to
1090    /// close the connection.
1091    pub id: i64,
1092    pub op: ProtocolOp,
1093    pub controls: Vec<Control>,
1094}
1095
1096impl LdapMessage {
1097    /// Parse exactly one message. The bytes must be one complete element, which
1098    /// is what [`crate::ber::element_size`] is for.
1099    pub fn parse(bytes: &[u8]) -> Result<LdapMessage> {
1100        let mut outer = Decoder::new(bytes);
1101        let mut message = outer.sequence()?;
1102        outer.finish("an LDAP message")?;
1103
1104        let id = message.integer()?;
1105        if id < 0 {
1106            return Err(Error::msg(format!(
1107                "the directory sent message id {id}; RFC 4511 says a message id is a positive \
1108                 integer or zero"
1109            )));
1110        }
1111
1112        let tag = message
1113            .peek_tag()
1114            .ok_or_else(|| Error::msg("an LDAP message carried an id but no operation"))?;
1115
1116        let op = match tag {
1117            tags::BIND_RESPONSE => {
1118                let mut fields = message.nested(tag)?;
1119                let result = LdapResult::decode(&mut fields)?;
1120                let credentials = optional_octets(&mut fields, ber::context(7, false))?;
1121                ProtocolOp::BindResponse { result, server_sasl_credentials: credentials }
1122            }
1123            tags::SEARCH_RESULT_ENTRY => {
1124                let mut fields = message.nested(tag)?;
1125                ProtocolOp::SearchResultEntry(SearchEntry::decode(&mut fields)?)
1126            }
1127            tags::SEARCH_RESULT_DONE => {
1128                let mut fields = message.nested(tag)?;
1129                ProtocolOp::SearchResultDone(LdapResult::decode(&mut fields)?)
1130            }
1131            tags::SEARCH_RESULT_REFERENCE => {
1132                let mut fields = message.nested(tag)?;
1133                let mut uris = Vec::new();
1134                while !fields.is_empty() {
1135                    uris.push(fields.string()?);
1136                }
1137                ProtocolOp::SearchResultReference(uris)
1138            }
1139            tags::EXTENDED_RESPONSE => {
1140                let mut fields = message.nested(tag)?;
1141                let result = LdapResult::decode(&mut fields)?;
1142                let name = match fields.has_tag(ber::context(10, false)) {
1143                    true => Some(fields.tagged_string(ber::context(10, false))?),
1144                    false => None,
1145                };
1146                let value = optional_octets(&mut fields, ber::context(11, false))?;
1147                ProtocolOp::ExtendedResponse { result, name, value }
1148            }
1149            tags::INTERMEDIATE_RESPONSE => {
1150                message.skip()?;
1151                ProtocolOp::IntermediateResponse
1152            }
1153            other => {
1154                message.skip()?;
1155                ProtocolOp::Unrecognised { tag: other }
1156            }
1157        };
1158
1159        let mut controls = Vec::new();
1160        if message.has_tag(ber::context(0, true)) {
1161            let mut list = message.nested(ber::context(0, true))?;
1162            while !list.is_empty() {
1163                controls.push(Control::decode(&mut list)?);
1164            }
1165        }
1166
1167        message.finish("an LDAP message")?;
1168        Ok(LdapMessage { id, op, controls })
1169    }
1170}
1171
1172fn optional_octets(decoder: &mut Decoder<'_>, tag: u8) -> Result<Option<Vec<u8>>> {
1173    if decoder.has_tag(tag) {
1174        return Ok(Some(decoder.expect(tag)?.to_vec()));
1175    }
1176    Ok(None)
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181    use super::*;
1182
1183    fn filter_bytes(filter: &Filter) -> Vec<u8> {
1184        let mut encoder = Encoder::new();
1185        filter.encode(&mut encoder);
1186        encoder.into_bytes()
1187    }
1188
1189    #[test]
1190    fn an_anonymous_bind_is_the_message_every_reference_shows() {
1191        // The canonical LDAPMessage: id 1, BindRequest, version 3, empty name,
1192        // empty simple password. Fourteen bytes, and every one of them is
1193        // checked here because this is the message that proves the framing,
1194        // the application tag and the implicit context tag are all right.
1195        assert_eq!(
1196            Operation::anonymous_bind().encode(1),
1197            vec![
1198                0x30, 0x0c, // SEQUENCE, 12 bytes
1199                0x02, 0x01, 0x01, // messageID 1
1200                0x60, 0x07, // [APPLICATION 0] BindRequest, 7 bytes
1201                0x02, 0x01, 0x03, // version 3
1202                0x04, 0x00, // name ""
1203                0x80, 0x00, // simple [0] ""
1204            ]
1205        );
1206    }
1207
1208    #[test]
1209    fn a_simple_bind_carries_the_dn_and_the_password_unwrapped() {
1210        let bytes = Operation::simple_bind("cn=root", "hunter2").unwrap().encode(2);
1211
1212        assert_eq!(
1213            bytes,
1214            vec![
1215                0x30, 0x1a, //
1216                0x02, 0x01, 0x02, // messageID 2
1217                0x60, 0x15, // BindRequest
1218                0x02, 0x01, 0x03, // version 3
1219                0x04, 0x07, b'c', b'n', b'=', b'r', b'o', b'o', b't', //
1220                0x80, 0x07, b'h', b'u', b'n', b't', b'e', b'r', b'2',
1221            ]
1222        );
1223
1224        // Which is the whole reason the transport rule exists: the password is
1225        // right there, one hexdump away from anyone on the path.
1226        assert!(bytes.windows(7).any(|window| window == b"hunter2"));
1227        assert!(Operation::simple_bind("cn=root", "hunter2").unwrap().carries_password());
1228        assert!(!Operation::anonymous_bind().carries_password());
1229    }
1230
1231    #[test]
1232    fn an_empty_password_is_refused_before_it_reaches_the_wire() {
1233        // LDAP answers a simple bind with a zero-length password with
1234        // `success`, because the specification says that is an anonymous bind.
1235        // A login form that passes an empty password field straight through
1236        // therefore reports a successful login — which is the bug, and it has
1237        // shipped in real products.
1238        //
1239        // The refusal lives in the encoder rather than in a caller, so there is
1240        // no code path that reaches the socket without passing this.
1241        // `Operation` has no `Debug` — its bytes hold the password — so the
1242        // error comes out of a match rather than `unwrap_err`.
1243        let error = match Operation::simple_bind("uid=alice,dc=example,dc=test", "") {
1244            Ok(_) => panic!("an empty password must never build a bind"),
1245            Err(error) => error.to_string(),
1246        };
1247
1248        assert!(error.contains("anonymous"), "the error must say what an empty password becomes");
1249        assert!(error.contains("bind_anonymous"), "and name the deliberate alternative: {error}");
1250
1251        // A password of one space is a password. Only zero length is the trap.
1252        assert!(Operation::simple_bind("uid=alice,dc=example,dc=test", " ").is_ok());
1253    }
1254
1255    #[test]
1256    fn unbind_is_a_primitive_null_with_no_response() {
1257        assert_eq!(Operation::unbind().encode(3), vec![0x30, 0x05, 0x02, 0x01, 0x03, 0x42, 0x00]);
1258    }
1259
1260    #[test]
1261    fn start_tls_names_the_oid_from_rfc_4511() {
1262        let bytes = Operation::start_tls().encode(1);
1263        let oid = START_TLS_OID.as_bytes();
1264
1265        assert_eq!(&bytes[..5], &[0x30, 0x1d, 0x02, 0x01, 0x01]);
1266        assert_eq!(bytes[5], 0x77, "[APPLICATION 23] ExtendedRequest");
1267        assert_eq!(bytes[7], 0x80, "requestName [0], primitive");
1268        assert_eq!(bytes[8] as usize, oid.len());
1269        assert_eq!(&bytes[9..], oid);
1270    }
1271
1272    #[test]
1273    fn an_equality_filter_puts_the_value_in_a_length_delimited_string() {
1274        // (uid=alice): [3] AttributeValueAssertion, two octet strings.
1275        assert_eq!(
1276            filter_bytes(&Filter::equals("uid", "alice")),
1277            vec![
1278                0xa3, 0x0c, //
1279                0x04, 0x03, b'u', b'i', b'd', //
1280                0x04, 0x05, b'a', b'l', b'i', b'c', b'e',
1281            ]
1282        );
1283    }
1284
1285    #[test]
1286    fn an_asterisk_in_an_equality_value_stays_a_literal_asterisk() {
1287        // This is the property the whole typed-filter design exists for. In the
1288        // string form `(uid=*)` is a presence filter that matches every entry.
1289        // In BER it is an equality assertion whose value is one byte, 0x2a, and
1290        // a directory compares it as a byte: it matches the account literally
1291        // named `*`, which is to say nothing.
1292        assert_eq!(
1293            filter_bytes(&Filter::equals("uid", "*")),
1294            vec![0xa3, 0x08, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x01, 0x2a]
1295        );
1296
1297        // And a presence filter, which is what `(uid=*)` means in text, is a
1298        // different tag entirely — you cannot arrive at it by accident.
1299        assert_eq!(
1300            filter_bytes(&Filter::present("uid")),
1301            vec![0x87, 0x03, b'u', b'i', b'd']
1302        );
1303
1304        // Nor can an injected filter fragment escape its own octet string.
1305        let injected = Filter::equals("uid", "alice)(uid=admin");
1306        let bytes = filter_bytes(&injected);
1307        assert_eq!(bytes[0], 0xa3, "still one equality assertion");
1308        assert_eq!(bytes.iter().filter(|&&byte| byte == 0xa3).count(), 1);
1309    }
1310
1311    #[test]
1312    fn a_not_filter_wraps_a_whole_inner_filter_because_the_tag_is_explicit() {
1313        // [2] tags a CHOICE, and a tag on a CHOICE is always explicit. Getting
1314        // this wrong produces bytes a directory answers with protocolError.
1315        assert_eq!(
1316            filter_bytes(&!Filter::equals("cn", "x")),
1317            vec![0xa2, 0x09, 0xa3, 0x07, 0x04, 0x02, b'c', b'n', 0x04, 0x01, b'x']
1318        );
1319    }
1320
1321    #[test]
1322    fn and_and_or_replace_the_set_tag_rather_than_wrapping_it() {
1323        // IMPLICIT TAGS: [0] SET OF Filter is 0xa0 and there is no 0x31 inside.
1324        let bytes = filter_bytes(&Filter::and([Filter::present("a"), Filter::present("b")]));
1325        assert_eq!(bytes, vec![0xa0, 0x06, 0x87, 0x01, b'a', 0x87, 0x01, b'b']);
1326
1327        let bytes = filter_bytes(&Filter::or([Filter::present("a")]));
1328        assert_eq!(bytes, vec![0xa1, 0x03, 0x87, 0x01, b'a']);
1329    }
1330
1331    #[test]
1332    fn a_substring_filter_tags_each_part_by_position() {
1333        // (o=univ*of*mich*) — RFC 4515's own example, in BER.
1334        let filter = Filter::Substrings {
1335            attribute: "o".into(),
1336            initial: Some("univ".into()),
1337            any: vec!["of".into(), "mich".into()],
1338            last: None,
1339        };
1340
1341        assert_eq!(
1342            filter_bytes(&filter),
1343            vec![
1344                0xa4, 0x15, //
1345                0x04, 0x01, b'o', //
1346                0x30, 0x10, // SEQUENCE OF substring
1347                0x80, 0x04, b'u', b'n', b'i', b'v', // initial [0]
1348                0x81, 0x02, b'o', b'f', // any [1]
1349                0x81, 0x04, b'm', b'i', b'c', b'h', // any [1]
1350            ]
1351        );
1352        assert_eq!(filter.to_string(), "(o=univ*of*mich*)");
1353    }
1354
1355    #[test]
1356    fn filters_print_the_examples_from_rfc_4515() {
1357        // Section 4 of RFC 4515, which is the only place the string form is
1358        // written down with worked examples.
1359        assert_eq!(Filter::equals("cn", "Babs Jensen").to_string(), "(cn=Babs Jensen)");
1360        assert_eq!(Filter::equals("cn", "Tim Howes").negate().to_string(), "(!(cn=Tim Howes))");
1361        // The two spellings are the same filter.
1362        assert_eq!(!Filter::equals("cn", "Tim Howes"), Filter::equals("cn", "Tim Howes").negate());
1363        assert_eq!(
1364            Filter::and([
1365                Filter::equals("objectClass", "Person"),
1366                Filter::or([
1367                    Filter::equals("sn", "Jensen"),
1368                    Filter::starts_with("cn", "Babs J"),
1369                ]),
1370            ])
1371            .to_string(),
1372            "(&(objectClass=Person)(|(sn=Jensen)(cn=Babs J*)))"
1373        );
1374        assert_eq!(Filter::equals("seeAlso", "").to_string(), "(seeAlso=)");
1375        assert_eq!(Filter::present("objectClass").to_string(), "(objectClass=*)");
1376
1377        // RFC 4526's absolute true and false filters.
1378        assert_eq!(Filter::and([]).to_string(), "(&)");
1379        assert_eq!(Filter::or([]).to_string(), "(|)");
1380    }
1381
1382    #[test]
1383    fn escaping_matches_rfc_4515_s_worked_examples() {
1384        // Every one of these is verbatim from RFC 4515 §4.
1385        assert_eq!(
1386            Filter::equals("o", "Parens R Us (for all your parenthetical needs)").to_string(),
1387            "(o=Parens R Us \\28for all your parenthetical needs\\29)"
1388        );
1389        assert_eq!(Filter::contains("cn", "*").to_string(), "(cn=*\\2a*)");
1390        assert_eq!(
1391            Filter::equals("filename", "C:\\MyFile").to_string(),
1392            "(filename=C:\\5cMyFile)"
1393        );
1394        // RFC 4515's `(bin=\00\00\00\04)`. The NULs must be escaped; the 0x04
1395        // may be, and this escapes only what the grammar forbids raw.
1396        assert_eq!(escape_filter_value("\0\0\0\u{4}"), "\\00\\00\\00\u{4}");
1397    }
1398
1399    #[test]
1400    fn an_unescaped_asterisk_would_have_matched_everybody() {
1401        // The bug, stated as a test. Pasting a username into a template is what
1402        // real code does, and `*` is a username anyone can type.
1403        let username = "*";
1404        let naive = format!("(uid={username})");
1405        assert_eq!(naive, "(uid=*)", "which is a presence filter — every account matches");
1406
1407        let escaped = format!("(uid={})", escape_filter_value(username));
1408        assert_eq!(escaped, "(uid=\\2a)", "which matches an account literally named *");
1409
1410        // And the same for the injection that rewrites the filter outright.
1411        assert_eq!(
1412            escape_filter_value("alice)(uid=admin"),
1413            "alice\\29\\28uid=admin",
1414            "the parentheses have to go, or the filter grows a second clause"
1415        );
1416    }
1417
1418    #[test]
1419    fn dn_escaping_covers_what_rfc_4514_requires() {
1420        assert_eq!(escape_dn_value("Smith, John"), "Smith\\, John");
1421        assert_eq!(escape_dn_value("a+b"), "a\\+b");
1422        assert_eq!(escape_dn_value("back\\slash"), "back\\\\slash");
1423        // Leading and trailing spaces, and a leading hash, are positional.
1424        assert_eq!(escape_dn_value(" alice "), "\\ alice\\ ");
1425        assert_eq!(escape_dn_value("#alice"), "\\#alice");
1426        assert_eq!(escape_dn_value("mid#dle"), "mid#dle");
1427        // The injection this prevents: a comma ends the RDN, so an unescaped
1428        // one moves the bind to an entry the template never described.
1429        assert_eq!(
1430            escape_dn_value("alice,ou=admins"),
1431            "alice\\,ou\\=admins"
1432        );
1433    }
1434
1435    #[test]
1436    fn a_search_request_encodes_every_field_in_order() {
1437        let request = SearchRequest::new("dc=example,dc=test", Filter::equals("uid", "alice"))
1438            .scope(Scope::Subtree)
1439            .size_limit(2)
1440            .attributes(["cn"]);
1441
1442        assert_eq!(
1443            Operation::search(&request).encode(4),
1444            vec![
1445                0x30, 0x3c, //
1446                0x02, 0x01, 0x04, // messageID 4
1447                0x63, 0x37, // [APPLICATION 3] SearchRequest
1448                0x04, 0x12, b'd', b'c', b'=', b'e', b'x', b'a', b'm', b'p', b'l', b'e', b',',
1449                b'd', b'c', b'=', b't', b'e', b's', b't', //
1450                0x0a, 0x01, 0x02, // scope wholeSubtree
1451                0x0a, 0x01, 0x00, // derefAliases neverDerefAliases
1452                0x02, 0x01, 0x02, // sizeLimit 2
1453                0x02, 0x01, 0x00, // timeLimit 0
1454                0x01, 0x01, 0x00, // typesOnly FALSE
1455                0xa3, 0x0c, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x05, b'a', b'l', b'i', b'c',
1456                b'e', // filter
1457                0x30, 0x04, 0x04, 0x02, b'c', b'n', // attributes
1458            ]
1459        );
1460    }
1461
1462    #[test]
1463    fn a_bind_response_decodes_its_code_and_the_directory_s_own_words() {
1464        // resultCode invalidCredentials, no matched DN, a diagnostic.
1465        let mut encoder = Encoder::new();
1466        encoder.sequence(|body| {
1467            body.integer(2);
1468            body.constructed(tags::BIND_RESPONSE, |fields| {
1469                fields.enumerated(49);
1470                fields.string("");
1471                fields.string("80090308: LdapErr: DSID-0C0903A9, comment: ..., data 533, v2580");
1472            });
1473        });
1474
1475        let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1476        assert_eq!(message.id, 2);
1477
1478        let ProtocolOp::BindResponse { result, .. } = &message.op else {
1479            panic!("expected a bind response, got {}", message.op.name());
1480        };
1481        assert_eq!(result.code, ResultCode::InvalidCredentials);
1482        assert!(result.diagnostic.contains("data 533"));
1483        assert!(!result.is_success());
1484    }
1485
1486    #[test]
1487    fn a_search_result_entry_decodes_its_dn_and_multi_valued_attributes() {
1488        let mut encoder = Encoder::new();
1489        encoder.sequence(|body| {
1490            body.integer(5);
1491            body.constructed(tags::SEARCH_RESULT_ENTRY, |entry| {
1492                entry.string("uid=alice,ou=people,dc=example,dc=test");
1493                entry.sequence(|attributes| {
1494                    attributes.sequence(|attribute| {
1495                        attribute.string("cn");
1496                        attribute.set(|values| {
1497                            values.octet_string(b"Alice Liddell");
1498                        });
1499                    });
1500                    attributes.sequence(|attribute| {
1501                        attribute.string("objectClass");
1502                        attribute.set(|values| {
1503                            values.octet_string(b"top");
1504                            values.octet_string(b"inetOrgPerson");
1505                        });
1506                    });
1507                });
1508            });
1509        });
1510
1511        let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1512        let ProtocolOp::SearchResultEntry(entry) = &message.op else {
1513            panic!("expected an entry, got {}", message.op.name());
1514        };
1515
1516        assert_eq!(entry.dn, "uid=alice,ou=people,dc=example,dc=test");
1517        assert_eq!(entry.value("cn"), Some("Alice Liddell"));
1518        // Attribute descriptions are case-insensitive, and directories differ
1519        // about which case they send back.
1520        assert_eq!(entry.value("CN"), Some("Alice Liddell"));
1521        assert_eq!(entry.values("objectclass"), vec!["top", "inetOrgPerson"]);
1522        assert_eq!(entry.value("mail"), None);
1523    }
1524
1525    #[test]
1526    fn a_referral_in_a_result_is_read_and_not_followed() {
1527        let mut encoder = Encoder::new();
1528        encoder.sequence(|body| {
1529            body.integer(6);
1530            body.constructed(tags::SEARCH_RESULT_DONE, |fields| {
1531                fields.enumerated(10);
1532                fields.string("dc=example,dc=test");
1533                fields.string("");
1534                fields.constructed(ber::context(3, true), |referral| {
1535                    referral.string("ldap://other.example.test/dc=example,dc=test");
1536                });
1537            });
1538        });
1539
1540        let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1541        let ProtocolOp::SearchResultDone(result) = &message.op else {
1542            panic!("expected a search result");
1543        };
1544        assert_eq!(result.code, ResultCode::Referral);
1545        assert_eq!(result.referrals, vec!["ldap://other.example.test/dc=example,dc=test"]);
1546        assert_eq!(result.matched_dn, "dc=example,dc=test");
1547    }
1548
1549    #[test]
1550    fn controls_round_trip_on_a_request_and_a_response() {
1551        let operation = Operation::search(&SearchRequest::new("", Filter::present("objectClass")))
1552            .with_control(Control::new("1.2.840.113556.1.4.319").critical(true).value(vec![1, 2]));
1553
1554        // The controls ride in [0] on the message, after the operation. Parsed
1555        // back, the search request itself is an operation this client does not
1556        // decode — which is exactly the case where the controls must still be
1557        // found, since they are read positionally after the operation.
1558        let bytes = operation.encode(9);
1559        let parsed = LdapMessage::parse(&bytes).unwrap();
1560        assert_eq!(parsed.id, 9);
1561        assert!(matches!(parsed.op, ProtocolOp::Unrecognised { .. }));
1562        assert_eq!(parsed.controls.len(), 1);
1563        assert_eq!(parsed.controls[0].oid, "1.2.840.113556.1.4.319");
1564        assert!(parsed.controls[0].critical);
1565        assert_eq!(parsed.controls[0].value, Some(vec![1, 2]));
1566
1567        let mut encoder = Encoder::new();
1568        encoder.sequence(|body| {
1569            body.integer(9);
1570            body.constructed(tags::SEARCH_RESULT_DONE, |fields| {
1571                fields.enumerated(0);
1572                fields.string("");
1573                fields.string("");
1574            });
1575            body.constructed(ber::context(0, true), |list| {
1576                list.sequence(|control| {
1577                    control.string("1.2.840.113556.1.4.319");
1578                    control.octet_string(&[7]);
1579                });
1580            });
1581        });
1582
1583        let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1584        assert_eq!(message.controls.len(), 1);
1585        assert_eq!(message.controls[0].oid, "1.2.840.113556.1.4.319");
1586        assert!(!message.controls[0].critical, "criticality DEFAULT FALSE when absent");
1587        assert_eq!(message.controls[0].value, Some(vec![7]));
1588    }
1589
1590    #[test]
1591    fn an_operation_this_client_never_asked_for_is_named_not_guessed_at() {
1592        let mut encoder = Encoder::new();
1593        encoder.sequence(|body| {
1594            body.integer(1);
1595            // [APPLICATION 7] ModifyResponse — well formed, and not ours.
1596            body.constructed(ber::application(7, true), |fields| {
1597                fields.enumerated(0);
1598                fields.string("");
1599                fields.string("");
1600            });
1601        });
1602
1603        let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1604        assert!(matches!(message.op, ProtocolOp::Unrecognised { tag: 0x67 }));
1605        assert!(message.op.result().is_none());
1606    }
1607
1608    #[test]
1609    fn a_truncated_or_corrupt_message_is_an_error_and_never_a_panic() {
1610        let mut encoder = Encoder::new();
1611        encoder.sequence(|body| {
1612            body.integer(1);
1613            body.constructed(tags::BIND_RESPONSE, |fields| {
1614                fields.enumerated(0);
1615                fields.string("");
1616                fields.string("");
1617            });
1618        });
1619        let complete = encoder.into_bytes();
1620
1621        for length in 0..complete.len() {
1622            assert!(
1623                LdapMessage::parse(&complete[..length]).is_err(),
1624                "a {length} byte prefix should not parse"
1625            );
1626        }
1627        assert!(LdapMessage::parse(&complete).is_ok());
1628
1629        // Trailing bytes after a complete message are a second message, and
1630        // this function parses one.
1631        let mut extra = complete.clone();
1632        extra.push(0x00);
1633        assert!(LdapMessage::parse(&extra).is_err());
1634    }
1635
1636    #[test]
1637    fn result_codes_round_trip_and_name_themselves() {
1638        for code in [0i64, 1, 32, 49, 50, 53, 80] {
1639            assert_eq!(ResultCode::from_i64(code).as_i64(), code);
1640        }
1641        assert_eq!(ResultCode::from_i64(49), ResultCode::InvalidCredentials);
1642        assert_eq!(ResultCode::from_i64(32), ResultCode::NoSuchObject);
1643        assert_eq!(ResultCode::from_i64(4242), ResultCode::Unknown(4242));
1644        assert_eq!(ResultCode::InvalidCredentials.to_string(), "invalidCredentials (49)");
1645        assert_eq!(ResultCode::Unknown(4242).as_i64(), 4242);
1646        assert!(ResultCode::Success.is_success());
1647    }
1648
1649    #[test]
1650    fn a_failed_result_explains_itself_in_words() {
1651        let result = LdapResult {
1652            code: ResultCode::NoSuchObject,
1653            matched_dn: "dc=example,dc=test".into(),
1654            diagnostic: "no such object".into(),
1655            referrals: Vec::new(),
1656        };
1657
1658        let error = result.into_result("search").unwrap_err().to_string();
1659        assert!(error.contains("noSuchObject (32)"), "got {error}");
1660        assert!(error.contains("no such object"), "got {error}");
1661        assert!(error.contains("dc=example,dc=test"), "the matched DN is the clue: {error}");
1662    }
1663}