⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/net/ldap/
client.rs

1//! LDAP v3 Client (RFC 4511)
2//!
3//! Implements the Lightweight Directory Access Protocol version 3, encoding
4//! all messages as ASN.1/BER via `crate::net::asn1`. Supports simple bind,
5//! search with filters, compare, modify, add, delete, and unbind operations.
6//!
7//! # Active Directory Integration
8//!
9//! `LdapAdClient` wraps `LdapClient` with AD-specific helpers for user
10//! lookup, group membership extraction, and bind-based authentication.
11//!
12//! # Credential Caching
13//!
14//! `CredentialCache` provides TTL-based caching of bind credentials to avoid
15//! repeated LDAP binds for recently authenticated users.
16
17#![allow(dead_code)]
18
19#[cfg(feature = "alloc")]
20extern crate alloc;
21
22#[cfg(feature = "alloc")]
23use alloc::{
24    collections::BTreeMap,
25    string::{String, ToString},
26    vec,
27    vec::Vec,
28};
29
30use crate::{
31    error::KernelError,
32    net::asn1::{
33        encode_application, encode_context_specific, encode_length, AsnDecoder, AsnEncoder,
34        AsnValue,
35    },
36};
37
38// ---------------------------------------------------------------------------
39// LDAP Constants
40// ---------------------------------------------------------------------------
41
42/// Default LDAP port
43pub const LDAP_PORT: u16 = 389;
44
45/// Default LDAPS port
46pub const LDAPS_PORT: u16 = 636;
47
48/// LDAP protocol version
49const LDAP_VERSION: i64 = 3;
50
51// LDAP operation tags (application-specific)
52const TAG_BIND_REQUEST: u8 = 0;
53const TAG_BIND_RESPONSE: u8 = 1;
54const TAG_UNBIND_REQUEST: u8 = 2;
55const TAG_SEARCH_REQUEST: u8 = 3;
56const TAG_SEARCH_RESULT_ENTRY: u8 = 4;
57const TAG_SEARCH_RESULT_DONE: u8 = 5;
58const TAG_MODIFY_REQUEST: u8 = 6;
59const TAG_MODIFY_RESPONSE: u8 = 7;
60const TAG_ADD_REQUEST: u8 = 8;
61const TAG_ADD_RESPONSE: u8 = 9;
62const TAG_DEL_REQUEST: u8 = 10;
63const TAG_DEL_RESPONSE: u8 = 11;
64const TAG_COMPARE_REQUEST: u8 = 14;
65const TAG_COMPARE_RESPONSE: u8 = 15;
66
67// ---------------------------------------------------------------------------
68// LDAP Result Codes
69// ---------------------------------------------------------------------------
70
71/// LDAP result codes (RFC 4511 Section 4.1.9)
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[repr(u8)]
74pub enum LdapResultCode {
75    /// Operation completed successfully
76    Success = 0,
77    /// Server internal error
78    OperationsError = 1,
79    /// Protocol violation
80    ProtocolError = 2,
81    /// Time limit exceeded
82    TimeLimitExceeded = 3,
83    /// Size limit exceeded
84    SizeLimitExceeded = 4,
85    /// Comparison returned false
86    CompareFalse = 5,
87    /// Comparison returned true
88    CompareTrue = 6,
89    /// Unsupported authentication method
90    AuthMethodNotSupported = 7,
91    /// Stronger auth required
92    StrongerAuthRequired = 8,
93    /// No such object in directory
94    NoSuchObject = 32,
95    /// Invalid credentials (wrong password)
96    InvalidCredentials = 49,
97    /// Insufficient access rights
98    InsufficientAccess = 50,
99    /// Server is busy
100    Busy = 51,
101    /// Server is unavailable
102    Unavailable = 52,
103    /// Server is unwilling to perform
104    UnwillingToPerform = 53,
105    /// Entry already exists
106    EntryAlreadyExists = 68,
107    /// Other / unknown error
108    Other = 80,
109}
110
111impl LdapResultCode {
112    /// Create from an integer result code.
113    fn from_i64(code: i64) -> Self {
114        match code {
115            0 => LdapResultCode::Success,
116            1 => LdapResultCode::OperationsError,
117            2 => LdapResultCode::ProtocolError,
118            3 => LdapResultCode::TimeLimitExceeded,
119            4 => LdapResultCode::SizeLimitExceeded,
120            5 => LdapResultCode::CompareFalse,
121            6 => LdapResultCode::CompareTrue,
122            7 => LdapResultCode::AuthMethodNotSupported,
123            8 => LdapResultCode::StrongerAuthRequired,
124            32 => LdapResultCode::NoSuchObject,
125            49 => LdapResultCode::InvalidCredentials,
126            50 => LdapResultCode::InsufficientAccess,
127            51 => LdapResultCode::Busy,
128            52 => LdapResultCode::Unavailable,
129            53 => LdapResultCode::UnwillingToPerform,
130            68 => LdapResultCode::EntryAlreadyExists,
131            _ => LdapResultCode::Other,
132        }
133    }
134}
135
136// ---------------------------------------------------------------------------
137// Search Scope and Filter
138// ---------------------------------------------------------------------------
139
140/// LDAP search scope
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142#[repr(u8)]
143pub enum SearchScope {
144    /// Search only the base object
145    BaseObject = 0,
146    /// Search one level below base
147    SingleLevel = 1,
148    /// Search entire subtree
149    WholeSubtree = 2,
150}
151
152/// LDAP search filter (RFC 4511 Section 4.5.1)
153#[cfg(feature = "alloc")]
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum LdapFilter {
156    /// AND of multiple filters (context tag 0)
157    And(Vec<LdapFilter>),
158    /// OR of multiple filters (context tag 1)
159    Or(Vec<LdapFilter>),
160    /// NOT of a filter (context tag 2)
161    Not(Vec<u8>),
162    /// Attribute equals value (context tag 3)
163    EqualityMatch(String, String),
164    /// Substring match (context tag 4)
165    Substrings(String, Option<String>, Vec<String>, Option<String>),
166    /// Attribute >= value (context tag 5)
167    GreaterOrEqual(String, String),
168    /// Attribute <= value (context tag 6)
169    LessOrEqual(String, String),
170    /// Attribute is present (context tag 7)
171    Present(String),
172    /// Approximate match (context tag 8)
173    ApproxMatch(String, String),
174}
175
176/// Modify operation type
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178#[repr(u8)]
179pub enum ModifyOperation {
180    /// Add attribute values
181    Add = 0,
182    /// Delete attribute values
183    Delete = 1,
184    /// Replace attribute values
185    Replace = 2,
186}
187
188// ---------------------------------------------------------------------------
189// Search Entry
190// ---------------------------------------------------------------------------
191
192/// A single search result entry
193#[cfg(feature = "alloc")]
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct SearchEntry {
196    /// Distinguished name
197    pub dn: String,
198    /// Attribute name -> values
199    pub attributes: BTreeMap<String, Vec<String>>,
200}
201
202#[cfg(feature = "alloc")]
203impl SearchEntry {
204    /// Create a new empty search entry with the given DN.
205    pub fn new(dn: &str) -> Self {
206        Self {
207            dn: String::from(dn),
208            attributes: BTreeMap::new(),
209        }
210    }
211
212    /// Get the first value of an attribute, if present.
213    pub fn get_first(&self, attr: &str) -> Option<&str> {
214        self.attributes
215            .get(attr)
216            .and_then(|vals| vals.first())
217            .map(|s| s.as_str())
218    }
219
220    /// Get all values of an attribute.
221    pub fn get_all(&self, attr: &str) -> Option<&Vec<String>> {
222        self.attributes.get(attr)
223    }
224}
225
226// ---------------------------------------------------------------------------
227// LDAP Client
228// ---------------------------------------------------------------------------
229
230/// LDAP client for directory operations.
231///
232/// Encodes requests and decodes responses using ASN.1/BER. Does not manage
233/// network transport directly; callers provide a send/receive mechanism.
234#[cfg(feature = "alloc")]
235pub struct LdapClient {
236    /// Monotonically increasing message ID
237    next_message_id: u32,
238    /// Whether a successful bind has occurred
239    bound: bool,
240    /// Base DN for searches
241    base_dn: String,
242    /// Bind DN (set after successful bind)
243    bind_dn: String,
244}
245
246#[cfg(feature = "alloc")]
247impl LdapClient {
248    /// Create a new LDAP client with the given base DN.
249    pub fn new(base_dn: &str) -> Self {
250        Self {
251            next_message_id: 1,
252            bound: false,
253            base_dn: String::from(base_dn),
254            bind_dn: String::new(),
255        }
256    }
257
258    /// Get and increment the message ID counter.
259    fn alloc_message_id(&mut self) -> u32 {
260        let id = self.next_message_id;
261        self.next_message_id = self.next_message_id.wrapping_add(1);
262        if self.next_message_id == 0 {
263            self.next_message_id = 1;
264        }
265        id
266    }
267
268    /// Whether the client is currently bound.
269    pub fn is_bound(&self) -> bool {
270        self.bound
271    }
272
273    /// Get the base DN.
274    pub fn base_dn(&self) -> &str {
275        &self.base_dn
276    }
277
278    // -----------------------------------------------------------------------
279    // Bind
280    // -----------------------------------------------------------------------
281
282    /// Encode a simple bind request (DN + password).
283    ///
284    /// Returns the BER-encoded LDAPMessage bytes.
285    pub fn encode_bind_request(&mut self, dn: &str, password: &str) -> Vec<u8> {
286        let msg_id = self.alloc_message_id();
287
288        // BindRequest ::= [APPLICATION 0] SEQUENCE {
289        //   version INTEGER,
290        //   name    LDAPDN,
291        //   authentication AuthenticationChoice }
292        // AuthenticationChoice ::= CHOICE {
293        //   simple [0] OCTET STRING }
294
295        let version = AsnEncoder::encode(&AsnValue::Integer(LDAP_VERSION));
296        let name = AsnEncoder::encode(&AsnValue::OctetString(dn.as_bytes().to_vec()));
297        let auth = encode_context_specific(0, false, password.as_bytes());
298
299        let mut bind_content = Vec::new();
300        bind_content.extend_from_slice(&version);
301        bind_content.extend_from_slice(&name);
302        bind_content.extend_from_slice(&auth);
303
304        let bind_request = encode_application(TAG_BIND_REQUEST, true, &bind_content);
305
306        self.bind_dn = String::from(dn);
307        self.encode_message(msg_id, &bind_request)
308    }
309
310    /// Parse a bind response. Returns the result code.
311    pub fn parse_bind_response(&mut self, data: &[u8]) -> Result<LdapResultCode, KernelError> {
312        let (_msg_id, op_tag, content) = self.decode_message_envelope(data)?;
313
314        if op_tag != TAG_BIND_RESPONSE {
315            return Err(KernelError::InvalidArgument {
316                name: "ldap_bind_response",
317                value: "unexpected operation tag",
318            });
319        }
320
321        let result_code = self.parse_ldap_result(&content)?;
322
323        if result_code == LdapResultCode::Success {
324            self.bound = true;
325        }
326
327        Ok(result_code)
328    }
329
330    /// Convenience: encode bind, parse response, return result.
331    pub fn bind(&mut self, dn: &str, password: &str) -> (Vec<u8>, LdapResultCode) {
332        let request = self.encode_bind_request(dn, password);
333        // In a real implementation, the request would be sent and response received.
334        // For now, return the encoded request and a placeholder result.
335        self.bound = true;
336        (request, LdapResultCode::Success)
337    }
338
339    // -----------------------------------------------------------------------
340    // Search
341    // -----------------------------------------------------------------------
342
343    /// Encode a search request.
344    ///
345    /// Returns BER-encoded LDAPMessage bytes.
346    pub fn encode_search_request(
347        &mut self,
348        base_dn: &str,
349        scope: SearchScope,
350        filter: &LdapFilter,
351        attributes: &[&str],
352    ) -> Vec<u8> {
353        let msg_id = self.alloc_message_id();
354
355        // SearchRequest ::= [APPLICATION 3] SEQUENCE {
356        //   baseObject   LDAPDN,
357        //   scope        ENUMERATED,
358        //   derefAliases ENUMERATED,
359        //   sizeLimit    INTEGER,
360        //   timeLimit    INTEGER,
361        //   typesOnly    BOOLEAN,
362        //   filter       Filter,
363        //   attributes   AttributeSelection }
364
365        let mut content = Vec::new();
366
367        // baseObject
368        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
369            base_dn.as_bytes().to_vec(),
370        )));
371        // scope
372        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Enumerated(scope as i64)));
373        // derefAliases (neverDerefAliases = 0)
374        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Enumerated(0)));
375        // sizeLimit (0 = no limit)
376        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Integer(0)));
377        // timeLimit (0 = no limit)
378        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Integer(0)));
379        // typesOnly
380        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Boolean(false)));
381        // filter
382        Self::encode_filter(filter, &mut content);
383        // attributes
384        let attr_values: Vec<AsnValue> = attributes
385            .iter()
386            .map(|a| AsnValue::OctetString(a.as_bytes().to_vec()))
387            .collect();
388        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Sequence(attr_values)));
389
390        let search_request = encode_application(TAG_SEARCH_REQUEST, true, &content);
391        self.encode_message(msg_id, &search_request)
392    }
393
394    /// Parse search result entries from a response buffer.
395    ///
396    /// A search operation may return multiple SearchResultEntry messages
397    /// followed by a SearchResultDone. This parses a single message.
398    pub fn decode_search_result(&self, data: &[u8]) -> Result<SearchResult, KernelError> {
399        let (_msg_id, op_tag, content) = self.decode_message_envelope(data)?;
400
401        match op_tag {
402            TAG_SEARCH_RESULT_ENTRY => {
403                let entry = self.parse_search_entry(&content)?;
404                Ok(SearchResult::Entry(entry))
405            }
406            TAG_SEARCH_RESULT_DONE => {
407                let code = self.parse_ldap_result(&content)?;
408                Ok(SearchResult::Done(code))
409            }
410            _ => Err(KernelError::InvalidArgument {
411                name: "ldap_search_result",
412                value: "unexpected operation tag",
413            }),
414        }
415    }
416
417    /// Convenience: build a search request with the client's base DN.
418    pub fn search(
419        &mut self,
420        scope: SearchScope,
421        filter: &LdapFilter,
422        attributes: &[&str],
423    ) -> Vec<u8> {
424        let base_dn = self.base_dn.clone();
425        self.encode_search_request(&base_dn, scope, filter, attributes)
426    }
427
428    // -----------------------------------------------------------------------
429    // Compare
430    // -----------------------------------------------------------------------
431
432    /// Encode a compare request.
433    pub fn encode_compare_request(&mut self, dn: &str, attribute: &str, value: &str) -> Vec<u8> {
434        let msg_id = self.alloc_message_id();
435
436        // CompareRequest ::= [APPLICATION 14] SEQUENCE {
437        //   entry LDAPDN,
438        //   ava   AttributeValueAssertion }
439
440        let mut content = Vec::new();
441        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
442            dn.as_bytes().to_vec(),
443        )));
444        // AttributeValueAssertion
445        let ava = AsnEncoder::encode(&AsnValue::Sequence(vec![
446            AsnValue::OctetString(attribute.as_bytes().to_vec()),
447            AsnValue::OctetString(value.as_bytes().to_vec()),
448        ]));
449        content.extend_from_slice(&ava);
450
451        let compare_request = encode_application(TAG_COMPARE_REQUEST, true, &content);
452        self.encode_message(msg_id, &compare_request)
453    }
454
455    // -----------------------------------------------------------------------
456    // Modify
457    // -----------------------------------------------------------------------
458
459    /// Encode a modify request.
460    ///
461    /// `modifications` is a list of `(operation, attribute, values)` tuples.
462    pub fn encode_modify_request(
463        &mut self,
464        dn: &str,
465        modifications: &[(ModifyOperation, &str, &[&str])],
466    ) -> Vec<u8> {
467        let msg_id = self.alloc_message_id();
468
469        let mut content = Vec::new();
470        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
471            dn.as_bytes().to_vec(),
472        )));
473
474        // Encode modifications as SEQUENCE OF SEQUENCE { operation, modification }
475        let mut mods = Vec::new();
476        for (op, attr, vals) in modifications {
477            let attr_vals: Vec<AsnValue> = vals
478                .iter()
479                .map(|v| AsnValue::OctetString(v.as_bytes().to_vec()))
480                .collect();
481            let modification = AsnValue::Sequence(vec![
482                AsnValue::Enumerated(*op as i64),
483                AsnValue::Sequence(vec![
484                    AsnValue::OctetString(attr.as_bytes().to_vec()),
485                    AsnValue::Set(attr_vals),
486                ]),
487            ]);
488            mods.push(modification);
489        }
490        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Sequence(mods)));
491
492        let modify_request = encode_application(TAG_MODIFY_REQUEST, true, &content);
493        self.encode_message(msg_id, &modify_request)
494    }
495
496    // -----------------------------------------------------------------------
497    // Add
498    // -----------------------------------------------------------------------
499
500    /// Encode an add request.
501    ///
502    /// `attributes` is a list of `(attribute_name, values)`.
503    pub fn encode_add_request(&mut self, dn: &str, attributes: &[(&str, &[&str])]) -> Vec<u8> {
504        let msg_id = self.alloc_message_id();
505
506        let mut content = Vec::new();
507        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
508            dn.as_bytes().to_vec(),
509        )));
510
511        let mut attr_list = Vec::new();
512        for (attr, vals) in attributes {
513            let attr_vals: Vec<AsnValue> = vals
514                .iter()
515                .map(|v| AsnValue::OctetString(v.as_bytes().to_vec()))
516                .collect();
517            attr_list.push(AsnValue::Sequence(vec![
518                AsnValue::OctetString(attr.as_bytes().to_vec()),
519                AsnValue::Set(attr_vals),
520            ]));
521        }
522        content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Sequence(attr_list)));
523
524        let add_request = encode_application(TAG_ADD_REQUEST, true, &content);
525        self.encode_message(msg_id, &add_request)
526    }
527
528    // -----------------------------------------------------------------------
529    // Delete
530    // -----------------------------------------------------------------------
531
532    /// Encode a delete request.
533    pub fn encode_delete_request(&mut self, dn: &str) -> Vec<u8> {
534        let msg_id = self.alloc_message_id();
535
536        // DelRequest ::= [APPLICATION 10] LDAPDN (primitive)
537        let del_request = encode_application(TAG_DEL_REQUEST, false, dn.as_bytes());
538        self.encode_message(msg_id, &del_request)
539    }
540
541    // -----------------------------------------------------------------------
542    // Unbind
543    // -----------------------------------------------------------------------
544
545    /// Encode an unbind request.
546    pub fn encode_unbind_request(&mut self) -> Vec<u8> {
547        let msg_id = self.alloc_message_id();
548        let unbind = encode_application(TAG_UNBIND_REQUEST, false, &[]);
549        self.bound = false;
550        self.encode_message(msg_id, &unbind)
551    }
552
553    /// Unbind and reset state.
554    pub fn unbind(&mut self) -> Vec<u8> {
555        self.encode_unbind_request()
556    }
557
558    // -----------------------------------------------------------------------
559    // Filter encoding
560    // -----------------------------------------------------------------------
561
562    /// Recursively encode an LDAP filter into ASN.1/BER bytes.
563    fn encode_filter(filter: &LdapFilter, out: &mut Vec<u8>) {
564        match filter {
565            LdapFilter::And(filters) => {
566                let mut content = Vec::new();
567                for f in filters {
568                    Self::encode_filter(f, &mut content);
569                }
570                out.extend_from_slice(&encode_context_specific(0, true, &content));
571            }
572            LdapFilter::Or(filters) => {
573                let mut content = Vec::new();
574                for f in filters {
575                    Self::encode_filter(f, &mut content);
576                }
577                out.extend_from_slice(&encode_context_specific(1, true, &content));
578            }
579            LdapFilter::Not(encoded) => {
580                out.extend_from_slice(&encode_context_specific(2, true, encoded));
581            }
582            LdapFilter::EqualityMatch(attr, val) => {
583                let content = AsnEncoder::encode(&AsnValue::Sequence(vec![
584                    AsnValue::OctetString(attr.as_bytes().to_vec()),
585                    AsnValue::OctetString(val.as_bytes().to_vec()),
586                ]));
587                // Extract inner content (skip SEQUENCE tag+length)
588                if content.len() > 2 {
589                    let inner = &content[2..]; // skip tag and short-form length
590                    out.extend_from_slice(&encode_context_specific(3, true, inner));
591                }
592            }
593            LdapFilter::Substrings(attr, initial, any, final_val) => {
594                let mut substr_content = Vec::new();
595                substr_content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
596                    attr.as_bytes().to_vec(),
597                )));
598                let mut substrings = Vec::new();
599                if let Some(init) = initial {
600                    substrings.push(AsnValue::ContextSpecific(0, init.as_bytes().to_vec()));
601                }
602                for a in any {
603                    substrings.push(AsnValue::ContextSpecific(1, a.as_bytes().to_vec()));
604                }
605                if let Some(fin) = final_val {
606                    substrings.push(AsnValue::ContextSpecific(2, fin.as_bytes().to_vec()));
607                }
608                substr_content
609                    .extend_from_slice(&AsnEncoder::encode(&AsnValue::Sequence(substrings)));
610                out.extend_from_slice(&encode_context_specific(4, true, &substr_content));
611            }
612            LdapFilter::GreaterOrEqual(attr, val) => {
613                let mut content = Vec::new();
614                content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
615                    attr.as_bytes().to_vec(),
616                )));
617                content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
618                    val.as_bytes().to_vec(),
619                )));
620                out.extend_from_slice(&encode_context_specific(5, true, &content));
621            }
622            LdapFilter::LessOrEqual(attr, val) => {
623                let mut content = Vec::new();
624                content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
625                    attr.as_bytes().to_vec(),
626                )));
627                content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
628                    val.as_bytes().to_vec(),
629                )));
630                out.extend_from_slice(&encode_context_specific(6, true, &content));
631            }
632            LdapFilter::Present(attr) => {
633                out.extend_from_slice(&encode_context_specific(7, false, attr.as_bytes()));
634            }
635            LdapFilter::ApproxMatch(attr, val) => {
636                let mut content = Vec::new();
637                content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
638                    attr.as_bytes().to_vec(),
639                )));
640                content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
641                    val.as_bytes().to_vec(),
642                )));
643                out.extend_from_slice(&encode_context_specific(8, true, &content));
644            }
645        }
646    }
647
648    // -----------------------------------------------------------------------
649    // Message envelope encoding/decoding
650    // -----------------------------------------------------------------------
651
652    /// Wrap an operation in an LDAPMessage envelope.
653    ///
654    /// LDAPMessage ::= SEQUENCE { messageID MessageID, protocolOp ... }
655    fn encode_message(&self, msg_id: u32, protocol_op: &[u8]) -> Vec<u8> {
656        let msg_id_encoded = AsnEncoder::encode(&AsnValue::Integer(msg_id as i64));
657        let mut content = Vec::new();
658        content.extend_from_slice(&msg_id_encoded);
659        content.extend_from_slice(protocol_op);
660
661        let mut out = Vec::new();
662        out.push(0x30); // SEQUENCE tag
663        encode_length(content.len(), &mut out);
664        out.extend_from_slice(&content);
665        out
666    }
667
668    /// Decode an LDAPMessage envelope.
669    ///
670    /// Returns `(message_id, operation_tag, operation_content)`.
671    fn decode_message_envelope(&self, data: &[u8]) -> Result<(u32, u8, Vec<u8>), KernelError> {
672        let (msg_value, _) = AsnDecoder::decode(data)?;
673
674        let items = match msg_value {
675            AsnValue::Sequence(items) => items,
676            _ => {
677                return Err(KernelError::InvalidArgument {
678                    name: "ldap_message",
679                    value: "not a sequence",
680                })
681            }
682        };
683
684        if items.len() < 2 {
685            return Err(KernelError::InvalidArgument {
686                name: "ldap_message",
687                value: "too few items",
688            });
689        }
690
691        let msg_id = match &items[0] {
692            AsnValue::Integer(n) => *n as u32,
693            _ => {
694                return Err(KernelError::InvalidArgument {
695                    name: "ldap_message_id",
696                    value: "not an integer",
697                })
698            }
699        };
700
701        // The protocol operation is encoded as an application-tagged value.
702        // Our decoder returns it as ContextSpecific(tag, content).
703        let (op_tag, op_content) = match &items[1] {
704            AsnValue::ContextSpecific(tag, content) => (*tag, content.clone()),
705            _ => {
706                return Err(KernelError::InvalidArgument {
707                    name: "ldap_protocol_op",
708                    value: "unexpected encoding",
709                })
710            }
711        };
712
713        Ok((msg_id, op_tag, op_content))
714    }
715
716    /// Parse an LDAPResult from content bytes.
717    ///
718    /// LDAPResult ::= SEQUENCE {
719    ///   resultCode ENUMERATED,
720    ///   matchedDN  LDAPDN,
721    ///   diagnosticMessage LDAPString,
722    ///   ... }
723    fn parse_ldap_result(&self, content: &[u8]) -> Result<LdapResultCode, KernelError> {
724        // The content is the inner bytes of the application-tagged value.
725        // Parse as a sequence of TLV elements.
726        let (first_val, _) = AsnDecoder::decode(content)?;
727
728        let code = match first_val {
729            AsnValue::Enumerated(n) => LdapResultCode::from_i64(n),
730            AsnValue::Integer(n) => LdapResultCode::from_i64(n),
731            _ => LdapResultCode::Other,
732        };
733
734        Ok(code)
735    }
736
737    /// Parse a SearchResultEntry.
738    fn parse_search_entry(&self, content: &[u8]) -> Result<SearchEntry, KernelError> {
739        // SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
740        //   objectName LDAPDN,
741        //   attributes PartialAttributeList }
742
743        let mut pos = 0;
744        let mut entry = SearchEntry::new("");
745
746        // objectName (OCTET STRING = DN)
747        if pos < content.len() {
748            let (dn_val, consumed) = AsnDecoder::decode(&content[pos..])?;
749            pos += consumed;
750            if let AsnValue::OctetString(bytes) = dn_val {
751                entry.dn = core::str::from_utf8(&bytes).unwrap_or("").to_string();
752            }
753        }
754
755        // attributes (SEQUENCE OF PartialAttribute)
756        if pos < content.len() {
757            let (attrs_val, _) = AsnDecoder::decode(&content[pos..])?;
758            if let AsnValue::Sequence(attrs) = attrs_val {
759                for attr in attrs {
760                    if let AsnValue::Sequence(parts) = attr {
761                        if parts.len() >= 2 {
762                            let attr_name = match &parts[0] {
763                                AsnValue::OctetString(b) => {
764                                    core::str::from_utf8(b).unwrap_or("").to_string()
765                                }
766                                _ => continue,
767                            };
768                            let mut values = Vec::new();
769                            if let AsnValue::Set(vals) = &parts[1] {
770                                for v in vals {
771                                    if let AsnValue::OctetString(b) = v {
772                                        if let Ok(s) = core::str::from_utf8(b) {
773                                            values.push(s.to_string());
774                                        }
775                                    }
776                                }
777                            }
778                            entry.attributes.insert(attr_name, values);
779                        }
780                    }
781                }
782            }
783        }
784
785        Ok(entry)
786    }
787}
788
789/// Result from parsing a search response message.
790#[cfg(feature = "alloc")]
791#[derive(Debug)]
792pub enum SearchResult {
793    /// A search result entry
794    Entry(SearchEntry),
795    /// Search is complete with the given result code
796    Done(LdapResultCode),
797}
798
799// ---------------------------------------------------------------------------
800// Active Directory Integration
801// ---------------------------------------------------------------------------
802
803/// Active Directory LDAP client wrapper.
804///
805/// Provides AD-specific operations like user lookup by sAMAccountName,
806/// group membership extraction, and bind-based authentication.
807#[cfg(feature = "alloc")]
808pub struct LdapAdClient {
809    /// Underlying LDAP client
810    client: LdapClient,
811    /// AD domain (e.g., "example.com")
812    domain: String,
813}
814
815#[cfg(feature = "alloc")]
816impl LdapAdClient {
817    /// Create a new AD client for the given domain and base DN.
818    pub fn new(domain: &str, base_dn: &str) -> Self {
819        Self {
820            client: LdapClient::new(base_dn),
821            domain: String::from(domain),
822        }
823    }
824
825    /// Get a reference to the underlying LDAP client.
826    pub fn client(&self) -> &LdapClient {
827        &self.client
828    }
829
830    /// Get a mutable reference to the underlying LDAP client.
831    pub fn client_mut(&mut self) -> &mut LdapClient {
832        &mut self.client
833    }
834
835    /// Build a user search filter for sAMAccountName.
836    pub fn user_filter(username: &str) -> LdapFilter {
837        LdapFilter::And(vec![
838            LdapFilter::EqualityMatch(String::from("objectClass"), String::from("user")),
839            LdapFilter::EqualityMatch(String::from("sAMAccountName"), String::from(username)),
840        ])
841    }
842
843    /// Encode a search request for a specific user by sAMAccountName.
844    pub fn search_user(&mut self, username: &str) -> Vec<u8> {
845        let filter = Self::user_filter(username);
846        let attrs = &[
847            "dn",
848            "sAMAccountName",
849            "displayName",
850            "mail",
851            "memberOf",
852            "userAccountControl",
853        ];
854        self.client
855            .search(SearchScope::WholeSubtree, &filter, attrs)
856    }
857
858    /// Encode a bind request for user authentication.
859    ///
860    /// Uses the UPN format: `username@domain`.
861    pub fn authenticate_user(&mut self, username: &str, password: &str) -> Vec<u8> {
862        let mut upn = String::from(username);
863        upn.push('@');
864        upn.push_str(&self.domain);
865        self.client.encode_bind_request(&upn, password)
866    }
867
868    /// Build a filter to find groups for a user DN.
869    pub fn groups_filter(user_dn: &str) -> LdapFilter {
870        LdapFilter::And(vec![
871            LdapFilter::EqualityMatch(String::from("objectClass"), String::from("group")),
872            LdapFilter::EqualityMatch(String::from("member"), String::from(user_dn)),
873        ])
874    }
875
876    /// Encode a search request for groups containing the given user DN.
877    pub fn get_groups(&mut self, user_dn: &str) -> Vec<u8> {
878        let filter = Self::groups_filter(user_dn);
879        let attrs = &["cn", "distinguishedName"];
880        self.client
881            .search(SearchScope::WholeSubtree, &filter, attrs)
882    }
883
884    /// Extract group names from the memberOf attribute of a search entry.
885    pub fn extract_groups(entry: &SearchEntry) -> Vec<String> {
886        let mut groups = Vec::new();
887        if let Some(member_of) = entry.get_all("memberOf") {
888            for dn in member_of {
889                // Extract CN from "CN=GroupName,OU=..."
890                if let Some(cn) = Self::extract_cn(dn) {
891                    groups.push(cn);
892                }
893            }
894        }
895        groups
896    }
897
898    /// Extract the CN (common name) from a distinguished name.
899    fn extract_cn(dn: &str) -> Option<String> {
900        for component in dn.split(',') {
901            let trimmed = component.trim();
902            if trimmed.len() > 3 {
903                let prefix = &trimmed[..3];
904                if prefix.eq_ignore_ascii_case("CN=") {
905                    return Some(String::from(&trimmed[3..]));
906                }
907            }
908        }
909        None
910    }
911}
912
913// ---------------------------------------------------------------------------
914// Credential Cache
915// ---------------------------------------------------------------------------
916
917/// TTL-based credential cache for avoiding repeated LDAP binds.
918///
919/// Stores hashed credentials with expiry timestamps based on tick counts
920/// (no floating point).
921#[cfg(feature = "alloc")]
922pub struct CredentialCache {
923    /// Map from username to cached credential entry
924    entries: BTreeMap<String, CachedCredential>,
925    /// TTL in ticks for cached entries
926    ttl_ticks: u64,
927    /// Maximum number of cached entries
928    max_entries: usize,
929}
930
931/// A cached credential entry.
932#[cfg(feature = "alloc")]
933#[derive(Debug, Clone)]
934struct CachedCredential {
935    /// Hash of the password (for verification without re-bind)
936    password_hash: [u8; 32],
937    /// Tick count when this entry expires
938    expires_at: u64,
939    /// Distinguished name used for bind
940    bind_dn: String,
941}
942
943#[cfg(feature = "alloc")]
944impl CredentialCache {
945    /// Create a new credential cache.
946    ///
947    /// `ttl_ticks` is the number of timer ticks before an entry expires.
948    /// `max_entries` limits the cache size.
949    pub fn new(ttl_ticks: u64, max_entries: usize) -> Self {
950        Self {
951            entries: BTreeMap::new(),
952            ttl_ticks,
953            max_entries,
954        }
955    }
956
957    /// Check if a cached credential exists and is still valid.
958    ///
959    /// Returns the bind DN if the password hash matches.
960    pub fn lookup(
961        &self,
962        username: &str,
963        password_hash: &[u8; 32],
964        current_tick: u64,
965    ) -> Option<&str> {
966        if let Some(entry) = self.entries.get(username) {
967            if current_tick < entry.expires_at && entry.password_hash == *password_hash {
968                return Some(&entry.bind_dn);
969            }
970        }
971        None
972    }
973
974    /// Store a credential in the cache.
975    pub fn store(
976        &mut self,
977        username: &str,
978        password_hash: [u8; 32],
979        bind_dn: &str,
980        current_tick: u64,
981    ) {
982        // Evict expired entries if at capacity
983        if self.entries.len() >= self.max_entries {
984            self.purge_expired(current_tick);
985        }
986
987        // If still at capacity, remove the oldest entry
988        if self.entries.len() >= self.max_entries {
989            if let Some(oldest_key) = self
990                .entries
991                .iter()
992                .min_by_key(|(_, v)| v.expires_at)
993                .map(|(k, _)| k.clone())
994            {
995                self.entries.remove(&oldest_key);
996            }
997        }
998
999        self.entries.insert(
1000            String::from(username),
1001            CachedCredential {
1002                password_hash,
1003                expires_at: current_tick.saturating_add(self.ttl_ticks),
1004                bind_dn: String::from(bind_dn),
1005            },
1006        );
1007    }
1008
1009    /// Remove expired entries.
1010    pub fn purge_expired(&mut self, current_tick: u64) {
1011        self.entries.retain(|_, v| current_tick < v.expires_at);
1012    }
1013
1014    /// Remove a specific entry.
1015    pub fn remove(&mut self, username: &str) {
1016        self.entries.remove(username);
1017    }
1018
1019    /// Get the number of cached entries.
1020    pub fn len(&self) -> usize {
1021        self.entries.len()
1022    }
1023
1024    /// Check if the cache is empty.
1025    pub fn is_empty(&self) -> bool {
1026        self.entries.is_empty()
1027    }
1028
1029    /// Clear all entries.
1030    pub fn clear(&mut self) {
1031        self.entries.clear();
1032    }
1033}
1034
1035// ---------------------------------------------------------------------------
1036// Tests
1037// ---------------------------------------------------------------------------
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042
1043    #[test]
1044    fn test_ldap_client_creation() {
1045        let client = LdapClient::new("dc=example,dc=com");
1046        assert!(!client.is_bound());
1047        assert_eq!(client.base_dn(), "dc=example,dc=com");
1048    }
1049
1050    #[test]
1051    fn test_message_id_increment() {
1052        let mut client = LdapClient::new("dc=test,dc=com");
1053        let _ = client.encode_bind_request("cn=admin", "secret");
1054        let _ = client.encode_bind_request("cn=admin", "secret");
1055        // Message IDs should be 1 and 2
1056        assert!(client.next_message_id >= 3);
1057    }
1058
1059    #[test]
1060    fn test_encode_bind_request_not_empty() {
1061        let mut client = LdapClient::new("dc=test,dc=com");
1062        let data = client.encode_bind_request("cn=admin,dc=test,dc=com", "password");
1063        assert!(!data.is_empty());
1064        // Should start with SEQUENCE tag
1065        assert_eq!(data[0], 0x30);
1066    }
1067
1068    #[test]
1069    fn test_encode_search_request() {
1070        let mut client = LdapClient::new("dc=example,dc=com");
1071        let filter = LdapFilter::EqualityMatch(String::from("uid"), String::from("jdoe"));
1072        let data = client.encode_search_request(
1073            "ou=people,dc=example,dc=com",
1074            SearchScope::WholeSubtree,
1075            &filter,
1076            &["cn", "mail"],
1077        );
1078        assert!(!data.is_empty());
1079        assert_eq!(data[0], 0x30);
1080    }
1081
1082    #[test]
1083    fn test_encode_unbind() {
1084        let mut client = LdapClient::new("dc=test,dc=com");
1085        client.bound = true;
1086        let data = client.encode_unbind_request();
1087        assert!(!data.is_empty());
1088        assert!(!client.is_bound());
1089    }
1090
1091    #[test]
1092    fn test_encode_delete_request() {
1093        let mut client = LdapClient::new("dc=test,dc=com");
1094        let data = client.encode_delete_request("cn=user,dc=test,dc=com");
1095        assert!(!data.is_empty());
1096    }
1097
1098    #[test]
1099    fn test_encode_compare_request() {
1100        let mut client = LdapClient::new("dc=test,dc=com");
1101        let data =
1102            client.encode_compare_request("cn=user,dc=test,dc=com", "userPassword", "secret");
1103        assert!(!data.is_empty());
1104    }
1105
1106    #[test]
1107    fn test_encode_modify_request() {
1108        let mut client = LdapClient::new("dc=test,dc=com");
1109        let mods = [(ModifyOperation::Replace, "mail", &["new@test.com"][..])];
1110        let data = client.encode_modify_request("cn=user,dc=test,dc=com", &mods);
1111        assert!(!data.is_empty());
1112    }
1113
1114    #[test]
1115    fn test_encode_add_request() {
1116        let mut client = LdapClient::new("dc=test,dc=com");
1117        let attrs = [
1118            ("objectClass", &["inetOrgPerson"][..]),
1119            ("cn", &["Test User"][..]),
1120            ("sn", &["User"][..]),
1121        ];
1122        let data = client.encode_add_request("cn=Test User,dc=test,dc=com", &attrs);
1123        assert!(!data.is_empty());
1124    }
1125
1126    #[test]
1127    fn test_filter_present() {
1128        let mut content = Vec::new();
1129        LdapClient::encode_filter(
1130            &LdapFilter::Present(String::from("objectClass")),
1131            &mut content,
1132        );
1133        assert!(!content.is_empty());
1134        // Context-specific tag 7, primitive
1135        assert_eq!(content[0] & 0xE0, 0x80); // context-specific class
1136    }
1137
1138    #[test]
1139    fn test_filter_and() {
1140        let filter = LdapFilter::And(vec![
1141            LdapFilter::Present(String::from("cn")),
1142            LdapFilter::EqualityMatch(String::from("sn"), String::from("Doe")),
1143        ]);
1144        let mut content = Vec::new();
1145        LdapClient::encode_filter(&filter, &mut content);
1146        assert!(!content.is_empty());
1147    }
1148
1149    #[test]
1150    fn test_search_entry() {
1151        let mut entry = SearchEntry::new("cn=test,dc=example,dc=com");
1152        entry
1153            .attributes
1154            .insert(String::from("cn"), vec![String::from("test")]);
1155        entry
1156            .attributes
1157            .insert(String::from("mail"), vec![String::from("test@example.com")]);
1158
1159        assert_eq!(entry.get_first("cn"), Some("test"));
1160        assert_eq!(entry.get_first("mail"), Some("test@example.com"));
1161        assert_eq!(entry.get_first("nonexistent"), None);
1162    }
1163
1164    #[test]
1165    fn test_result_code_from_i64() {
1166        assert_eq!(LdapResultCode::from_i64(0), LdapResultCode::Success);
1167        assert_eq!(
1168            LdapResultCode::from_i64(49),
1169            LdapResultCode::InvalidCredentials
1170        );
1171        assert_eq!(LdapResultCode::from_i64(999), LdapResultCode::Other);
1172    }
1173
1174    // --- Active Directory tests ---
1175
1176    #[test]
1177    fn test_ad_client_creation() {
1178        let ad = LdapAdClient::new("example.com", "dc=example,dc=com");
1179        assert_eq!(ad.domain, "example.com");
1180        assert_eq!(ad.client().base_dn(), "dc=example,dc=com");
1181    }
1182
1183    #[test]
1184    fn test_ad_user_filter() {
1185        let filter = LdapAdClient::user_filter("jdoe");
1186        match filter {
1187            LdapFilter::And(filters) => {
1188                assert_eq!(filters.len(), 2);
1189            }
1190            _ => panic!("expected AND filter"),
1191        }
1192    }
1193
1194    #[test]
1195    fn test_ad_search_user() {
1196        let mut ad = LdapAdClient::new("example.com", "dc=example,dc=com");
1197        let data = ad.search_user("jdoe");
1198        assert!(!data.is_empty());
1199    }
1200
1201    #[test]
1202    fn test_ad_authenticate_user() {
1203        let mut ad = LdapAdClient::new("example.com", "dc=example,dc=com");
1204        let data = ad.authenticate_user("jdoe", "password123");
1205        assert!(!data.is_empty());
1206    }
1207
1208    #[test]
1209    fn test_ad_extract_cn() {
1210        let cn = LdapAdClient::extract_cn("CN=Domain Admins,OU=Groups,DC=example,DC=com");
1211        assert_eq!(cn, Some(String::from("Domain Admins")));
1212
1213        let cn = LdapAdClient::extract_cn("OU=NoCommonName,DC=example");
1214        assert_eq!(cn, None);
1215    }
1216
1217    #[test]
1218    fn test_ad_extract_groups() {
1219        let mut entry = SearchEntry::new("cn=jdoe,dc=example,dc=com");
1220        entry.attributes.insert(
1221            String::from("memberOf"),
1222            vec![
1223                String::from("CN=Developers,OU=Groups,DC=example,DC=com"),
1224                String::from("CN=VPN Users,OU=Groups,DC=example,DC=com"),
1225            ],
1226        );
1227        let groups = LdapAdClient::extract_groups(&entry);
1228        assert_eq!(groups.len(), 2);
1229        assert_eq!(groups[0], "Developers");
1230        assert_eq!(groups[1], "VPN Users");
1231    }
1232
1233    #[test]
1234    fn test_credential_cache_store_lookup() {
1235        let mut cache = CredentialCache::new(1000, 10);
1236        let hash = [0x42u8; 32];
1237        cache.store("alice", hash, "cn=alice,dc=test", 100);
1238        assert_eq!(cache.len(), 1);
1239
1240        // Valid lookup
1241        let result = cache.lookup("alice", &hash, 200);
1242        assert_eq!(result, Some("cn=alice,dc=test"));
1243
1244        // Wrong hash
1245        let wrong_hash = [0x00u8; 32];
1246        assert_eq!(cache.lookup("alice", &wrong_hash, 200), None);
1247
1248        // Expired
1249        assert_eq!(cache.lookup("alice", &hash, 1200), None);
1250    }
1251
1252    #[test]
1253    fn test_credential_cache_purge() {
1254        let mut cache = CredentialCache::new(100, 10);
1255        let hash = [0x42u8; 32];
1256        cache.store("alice", hash, "cn=alice", 0);
1257        cache.store("bob", hash, "cn=bob", 50);
1258
1259        assert_eq!(cache.len(), 2);
1260        cache.purge_expired(120);
1261        assert_eq!(cache.len(), 1); // only bob remains
1262
1263        cache.purge_expired(200);
1264        assert_eq!(cache.len(), 0);
1265    }
1266
1267    #[test]
1268    fn test_credential_cache_eviction() {
1269        let mut cache = CredentialCache::new(1000, 2);
1270        let hash = [0x42u8; 32];
1271        cache.store("alice", hash, "cn=alice", 0);
1272        cache.store("bob", hash, "cn=bob", 10);
1273        assert_eq!(cache.len(), 2);
1274
1275        // Adding a third should evict the oldest (alice)
1276        cache.store("charlie", hash, "cn=charlie", 20);
1277        assert_eq!(cache.len(), 2);
1278        assert_eq!(cache.lookup("alice", &hash, 20), None);
1279    }
1280
1281    #[test]
1282    fn test_credential_cache_remove() {
1283        let mut cache = CredentialCache::new(1000, 10);
1284        let hash = [0x42u8; 32];
1285        cache.store("alice", hash, "cn=alice", 0);
1286        assert_eq!(cache.len(), 1);
1287        cache.remove("alice");
1288        assert!(cache.is_empty());
1289    }
1290}