1#![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
38pub const LDAP_PORT: u16 = 389;
44
45pub const LDAPS_PORT: u16 = 636;
47
48const LDAP_VERSION: i64 = 3;
50
51const 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[repr(u8)]
74pub enum LdapResultCode {
75 Success = 0,
77 OperationsError = 1,
79 ProtocolError = 2,
81 TimeLimitExceeded = 3,
83 SizeLimitExceeded = 4,
85 CompareFalse = 5,
87 CompareTrue = 6,
89 AuthMethodNotSupported = 7,
91 StrongerAuthRequired = 8,
93 NoSuchObject = 32,
95 InvalidCredentials = 49,
97 InsufficientAccess = 50,
99 Busy = 51,
101 Unavailable = 52,
103 UnwillingToPerform = 53,
105 EntryAlreadyExists = 68,
107 Other = 80,
109}
110
111impl LdapResultCode {
112 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142#[repr(u8)]
143pub enum SearchScope {
144 BaseObject = 0,
146 SingleLevel = 1,
148 WholeSubtree = 2,
150}
151
152#[cfg(feature = "alloc")]
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum LdapFilter {
156 And(Vec<LdapFilter>),
158 Or(Vec<LdapFilter>),
160 Not(Vec<u8>),
162 EqualityMatch(String, String),
164 Substrings(String, Option<String>, Vec<String>, Option<String>),
166 GreaterOrEqual(String, String),
168 LessOrEqual(String, String),
170 Present(String),
172 ApproxMatch(String, String),
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178#[repr(u8)]
179pub enum ModifyOperation {
180 Add = 0,
182 Delete = 1,
184 Replace = 2,
186}
187
188#[cfg(feature = "alloc")]
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct SearchEntry {
196 pub dn: String,
198 pub attributes: BTreeMap<String, Vec<String>>,
200}
201
202#[cfg(feature = "alloc")]
203impl SearchEntry {
204 pub fn new(dn: &str) -> Self {
206 Self {
207 dn: String::from(dn),
208 attributes: BTreeMap::new(),
209 }
210 }
211
212 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 pub fn get_all(&self, attr: &str) -> Option<&Vec<String>> {
222 self.attributes.get(attr)
223 }
224}
225
226#[cfg(feature = "alloc")]
235pub struct LdapClient {
236 next_message_id: u32,
238 bound: bool,
240 base_dn: String,
242 bind_dn: String,
244}
245
246#[cfg(feature = "alloc")]
247impl LdapClient {
248 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 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 pub fn is_bound(&self) -> bool {
270 self.bound
271 }
272
273 pub fn base_dn(&self) -> &str {
275 &self.base_dn
276 }
277
278 pub fn encode_bind_request(&mut self, dn: &str, password: &str) -> Vec<u8> {
286 let msg_id = self.alloc_message_id();
287
288 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 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 pub fn bind(&mut self, dn: &str, password: &str) -> (Vec<u8>, LdapResultCode) {
332 let request = self.encode_bind_request(dn, password);
333 self.bound = true;
336 (request, LdapResultCode::Success)
337 }
338
339 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 let mut content = Vec::new();
366
367 content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
369 base_dn.as_bytes().to_vec(),
370 )));
371 content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Enumerated(scope as i64)));
373 content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Enumerated(0)));
375 content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Integer(0)));
377 content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Integer(0)));
379 content.extend_from_slice(&AsnEncoder::encode(&AsnValue::Boolean(false)));
381 Self::encode_filter(filter, &mut content);
383 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 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 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 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 let mut content = Vec::new();
441 content.extend_from_slice(&AsnEncoder::encode(&AsnValue::OctetString(
442 dn.as_bytes().to_vec(),
443 )));
444 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 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 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 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 pub fn encode_delete_request(&mut self, dn: &str) -> Vec<u8> {
534 let msg_id = self.alloc_message_id();
535
536 let del_request = encode_application(TAG_DEL_REQUEST, false, dn.as_bytes());
538 self.encode_message(msg_id, &del_request)
539 }
540
541 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 pub fn unbind(&mut self) -> Vec<u8> {
555 self.encode_unbind_request()
556 }
557
558 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 if content.len() > 2 {
589 let inner = &content[2..]; 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 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); encode_length(content.len(), &mut out);
664 out.extend_from_slice(&content);
665 out
666 }
667
668 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 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 fn parse_ldap_result(&self, content: &[u8]) -> Result<LdapResultCode, KernelError> {
724 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 fn parse_search_entry(&self, content: &[u8]) -> Result<SearchEntry, KernelError> {
739 let mut pos = 0;
744 let mut entry = SearchEntry::new("");
745
746 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 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#[cfg(feature = "alloc")]
791#[derive(Debug)]
792pub enum SearchResult {
793 Entry(SearchEntry),
795 Done(LdapResultCode),
797}
798
799#[cfg(feature = "alloc")]
808pub struct LdapAdClient {
809 client: LdapClient,
811 domain: String,
813}
814
815#[cfg(feature = "alloc")]
816impl LdapAdClient {
817 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 pub fn client(&self) -> &LdapClient {
827 &self.client
828 }
829
830 pub fn client_mut(&mut self) -> &mut LdapClient {
832 &mut self.client
833 }
834
835 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 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 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 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 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 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 if let Some(cn) = Self::extract_cn(dn) {
891 groups.push(cn);
892 }
893 }
894 }
895 groups
896 }
897
898 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#[cfg(feature = "alloc")]
922pub struct CredentialCache {
923 entries: BTreeMap<String, CachedCredential>,
925 ttl_ticks: u64,
927 max_entries: usize,
929}
930
931#[cfg(feature = "alloc")]
933#[derive(Debug, Clone)]
934struct CachedCredential {
935 password_hash: [u8; 32],
937 expires_at: u64,
939 bind_dn: String,
941}
942
943#[cfg(feature = "alloc")]
944impl CredentialCache {
945 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 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 pub fn store(
976 &mut self,
977 username: &str,
978 password_hash: [u8; 32],
979 bind_dn: &str,
980 current_tick: u64,
981 ) {
982 if self.entries.len() >= self.max_entries {
984 self.purge_expired(current_tick);
985 }
986
987 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 pub fn purge_expired(&mut self, current_tick: u64) {
1011 self.entries.retain(|_, v| current_tick < v.expires_at);
1012 }
1013
1014 pub fn remove(&mut self, username: &str) {
1016 self.entries.remove(username);
1017 }
1018
1019 pub fn len(&self) -> usize {
1021 self.entries.len()
1022 }
1023
1024 pub fn is_empty(&self) -> bool {
1026 self.entries.is_empty()
1027 }
1028
1029 pub fn clear(&mut self) {
1031 self.entries.clear();
1032 }
1033}
1034
1035#[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 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 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 assert_eq!(content[0] & 0xE0, 0x80); }
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 #[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 let result = cache.lookup("alice", &hash, 200);
1242 assert_eq!(result, Some("cn=alice,dc=test"));
1243
1244 let wrong_hash = [0x00u8; 32];
1246 assert_eq!(cache.lookup("alice", &wrong_hash, 200), None);
1247
1248 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); 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 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}