kanidmd_lib/idm/
ldap.rs

1//! LDAP specific operations handling components. This is where LDAP operations
2//! are sent to for processing.
3
4use std::collections::BTreeSet;
5use std::iter;
6use std::str::FromStr;
7
8use compact_jwt::JwsCompact;
9use kanidm_proto::constants::*;
10use kanidm_proto::internal::{ApiToken, UserAuthToken};
11use ldap3_proto::simple::*;
12use regex::{Regex, RegexBuilder};
13use std::net::IpAddr;
14use tracing::trace;
15use uuid::Uuid;
16
17use crate::event::SearchEvent;
18use crate::idm::event::{LdapApplicationAuthEvent, LdapAuthEvent, LdapTokenAuthEvent};
19use crate::idm::server::{IdmServer, IdmServerAuthTransaction, IdmServerTransaction};
20use crate::prelude::*;
21
22// Clippy doesn't like Bind here. But proto needs unboxed ldapmsg,
23// and ldapboundtoken is moved. Really, it's not too bad, every message here is pretty sucky.
24#[allow(clippy::large_enum_variant)]
25pub enum LdapResponseState {
26    Unbind,
27    Disconnect(LdapMsg),
28    Bind(LdapBoundToken, LdapMsg),
29    Respond(LdapMsg),
30    MultiPartResponse(Vec<LdapMsg>),
31    BindMultiPartResponse(LdapBoundToken, Vec<LdapMsg>),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum LdapSession {
36    // Maps through and provides anon read, but allows us to check the validity
37    // of the account still.
38    UnixBind(Uuid),
39    UserAuthToken(UserAuthToken),
40    ApiToken(ApiToken),
41    ApplicationPasswordBind(Uuid, Uuid),
42}
43
44#[derive(Debug, Clone)]
45pub struct LdapBoundToken {
46    // Used to help ID the user doing the action, makes logging nicer.
47    pub spn: String,
48    pub session_id: Uuid,
49    // This is the effective session permission. This is generated from either:
50    // * A valid anonymous bind
51    // * A valid unix pw bind
52    // * A valid ApiToken
53    // In a way, this is a stepping stone to an "ident" but allows us to check
54    // the session is still "valid" depending on it's origin.
55    pub effective_session: LdapSession,
56}
57
58pub struct LdapServer {
59    rootdse: LdapSearchResultEntry,
60    basedn: String,
61    dnre: Regex,
62    binddnre: Regex,
63    max_queryable_attrs: usize,
64}
65
66#[derive(Debug)]
67enum LdapBindTarget {
68    Account(Uuid),
69    ApiToken,
70    Application(String, Uuid),
71}
72
73impl LdapServer {
74    pub async fn new(idms: &IdmServer) -> Result<Self, OperationError> {
75        // let ct = duration_from_epoch_now();
76        let mut idms_prox_read = idms.proxy_read().await?;
77        // This is the rootdse path.
78        // get the domain_info item
79        let domain_entry = idms_prox_read
80            .qs_read
81            .internal_search_uuid(UUID_DOMAIN_INFO)?;
82
83        // Get the maximum number of queryable attributes from the domain entry
84        let max_queryable_attrs = domain_entry
85            .get_ava_single_uint32(Attribute::LdapMaxQueryableAttrs)
86            .map(|u| u as usize)
87            .unwrap_or(DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES);
88
89        let basedn = domain_entry
90            .get_ava_single_iutf8(Attribute::DomainLdapBasedn)
91            .map(|s| s.to_string())
92            .or_else(|| {
93                domain_entry
94                    .get_ava_single_iname(Attribute::DomainName)
95                    .map(ldap_domain_to_dc)
96            })
97            .ok_or(OperationError::InvalidEntryState)?;
98
99        // It is necessary to swap greed to avoid the first group "<attr>=<val>" matching the
100        // next group "app=<app>", son one can use "app=app1,dc=test,dc=net" as search base:
101        // Greedy (app=app1,dc=test,dc=net):
102        //     Match 1      - app=app1,dc=test,dc=net
103        //     Group 1      - app=app1,
104        //     Group <attr> - app
105        //     Group <val>  - app1
106        //     Group 6      - dc=test,dc=net
107        // Ungreedy (app=app1,dc=test,dc=net):
108        //     Match 1      - app=app1,dc=test,dc=net
109        //     Group 4      - app=app1,
110        //     Group <app>  - app1
111        //     Group 6      - dc=test,dc=net
112        let dnre = RegexBuilder::new(
113            format!("^((?P<attr>[^=,]+)=(?P<val>[^=,]+),)?(app=(?P<app>[^=,]+),)?({basedn})$")
114                .as_str(),
115        )
116        .swap_greed(true)
117        .build()
118        .map_err(|_| OperationError::InvalidEntryState)?;
119
120        let binddnre = Regex::new(
121            format!("^((([^=,]+)=)?(?P<val>[^=,]+))(,app=(?P<app>[^=,]+))?(,{basedn})?$").as_str(),
122        )
123        .map_err(|_| OperationError::InvalidEntryState)?;
124
125        let rootdse = LdapSearchResultEntry {
126            dn: "".to_string(),
127            attributes: vec![
128                LdapPartialAttribute {
129                    atype: ATTR_OBJECTCLASS.to_string(),
130                    vals: vec!["top".as_bytes().to_vec()],
131                },
132                LdapPartialAttribute {
133                    atype: "vendorname".to_string(),
134                    vals: vec!["Kanidm Project".as_bytes().to_vec()],
135                },
136                LdapPartialAttribute {
137                    atype: "vendorversion".to_string(),
138                    vals: vec![env!("CARGO_PKG_VERSION").as_bytes().to_vec()],
139                },
140                LdapPartialAttribute {
141                    atype: "supportedldapversion".to_string(),
142                    vals: vec!["3".as_bytes().to_vec()],
143                },
144                LdapPartialAttribute {
145                    atype: "supportedextension".to_string(),
146                    vals: vec!["1.3.6.1.4.1.4203.1.11.3".as_bytes().to_vec()],
147                },
148                LdapPartialAttribute {
149                    atype: "supportedfeatures".to_string(),
150                    vals: vec!["1.3.6.1.4.1.4203.1.5.1".as_bytes().to_vec()],
151                },
152                LdapPartialAttribute {
153                    atype: "defaultnamingcontext".to_string(),
154                    vals: vec![basedn.as_bytes().to_vec()],
155                },
156            ],
157        };
158
159        Ok(LdapServer {
160            rootdse,
161            basedn,
162            dnre,
163            binddnre,
164            max_queryable_attrs,
165        })
166    }
167
168    #[instrument(level = "debug", skip_all)]
169    async fn do_search(
170        &self,
171        idms: &IdmServer,
172        sr: &SearchRequest,
173        uat: &LdapBoundToken,
174        source: Source,
175        // eventid: &Uuid,
176    ) -> Result<Vec<LdapMsg>, OperationError> {
177        admin_info!("Attempt LDAP Search for {}", uat.spn);
178        // If the request is "", Base, Present(Attribute::ObjectClass.into()), [], then we want the rootdse.
179        if sr.base.is_empty() && sr.scope == LdapSearchScope::Base {
180            admin_info!("LDAP Search success - RootDSE");
181            Ok(vec![
182                sr.gen_result_entry(self.rootdse.clone()),
183                sr.gen_success(),
184            ])
185        } else {
186            // We want something else apparently. Need to do some more work ...
187            // Parse the operation and make sure it's sane before we start the txn.
188
189            // This scoping returns an extra filter component.
190
191            let (opt_attr, opt_value) = match self.dnre.captures(sr.base.as_str()) {
192                Some(caps) => (
193                    caps.name("attr").map(|v| v.as_str().to_string()),
194                    caps.name("val").map(|v| v.as_str().to_string()),
195                ),
196                None => {
197                    request_error!("LDAP Search failure - invalid basedn");
198                    return Err(OperationError::InvalidRequestState);
199                }
200            };
201
202            let req_dn = match (opt_attr, opt_value) {
203                (Some(a), Some(v)) => Some((a, v)),
204                (None, None) => None,
205                _ => {
206                    request_error!("LDAP Search failure - invalid rdn");
207                    return Err(OperationError::InvalidRequestState);
208                }
209            };
210
211            trace!(rdn = ?req_dn);
212
213            // Map the Some(a,v) to ...?
214
215            let ext_filter = match (&sr.scope, req_dn) {
216                // OneLevel and Child searches are **very** similar for us because child
217                // is a "subtree search excluding base". Because we don't have a tree structure at
218                // all, this is the same as a one level (all children of base excluding base).
219                (LdapSearchScope::Children, Some(_r)) | (LdapSearchScope::OneLevel, Some(_r)) => {
220                    return Ok(vec![sr.gen_success()])
221                }
222                (LdapSearchScope::Children, None) | (LdapSearchScope::OneLevel, None) => {
223                    // exclude domain_info
224                    Some(LdapFilter::Not(Box::new(LdapFilter::Equality(
225                        Attribute::Uuid.to_string(),
226                        STR_UUID_DOMAIN_INFO.to_string(),
227                    ))))
228                }
229                // because we request a specific DN, these are the same since we want the same
230                // entry.
231                (LdapSearchScope::Base, Some((a, v)))
232                | (LdapSearchScope::Subtree, Some((a, v))) => Some(LdapFilter::Equality(a, v)),
233                (LdapSearchScope::Base, None) => {
234                    // domain_info
235                    Some(LdapFilter::Equality(
236                        Attribute::Uuid.to_string(),
237                        STR_UUID_DOMAIN_INFO.to_string(),
238                    ))
239                }
240                (LdapSearchScope::Subtree, None) => {
241                    // No filter changes needed.
242                    None
243                }
244            };
245
246            let mut no_attrs = false;
247            let mut all_attrs = false;
248            let mut all_op_attrs = false;
249
250            let attrs_len = sr.attrs.len();
251            if sr.attrs.is_empty() {
252                // If [], then "all" attrs
253                all_attrs = true;
254            } else if attrs_len < self.max_queryable_attrs {
255                sr.attrs.iter().for_each(|a| {
256                    if a == "*" {
257                        all_attrs = true;
258                    } else if a == "+" {
259                        // This forces the BE to get all the attrs so we can
260                        // map all vattrs.
261                        all_attrs = true;
262                        all_op_attrs = true;
263                    } else if a == "1.1" {
264                        /*
265                         *  ref https://www.rfc-editor.org/rfc/rfc4511#section-4.5.1.8
266                         *
267                         *  A list containing only the OID "1.1" indicates that no
268                         *  attributes are to be returned. If "1.1" is provided with other
269                         *  attributeSelector values, the "1.1" attributeSelector is
270                         *  ignored. This OID was chosen because it does not (and can not)
271                         *  correspond to any attribute in use.
272                         */
273                        if sr.attrs.len() == 1 {
274                            no_attrs = true;
275                        }
276                    }
277                })
278            } else {
279                admin_error!(
280                    "Too many LDAP attributes requested. Maximum allowed is {}, while your search query had {}",
281                    self.max_queryable_attrs, attrs_len
282                );
283                return Err(OperationError::ResourceLimit);
284            }
285
286            // We need to retain this to know what the client requested.
287            let (k_attrs, l_attrs) = if no_attrs {
288                // Request no attributes and no mapped attributes.
289                (None, Vec::with_capacity(0))
290            } else if all_op_attrs {
291                // We need all attrs, and we do a full v_attr map.
292                (None, ldap_all_vattrs())
293            } else if all_attrs {
294                // We are already getting all attrs, but if there are any virtual attrs
295                // we need them in our request as well.
296                let req_attrs: Vec<String> = sr
297                    .attrs
298                    .iter()
299                    .filter_map(|a| {
300                        let a_lower = a.to_lowercase();
301
302                        if ldap_vattr_map(&a_lower).is_some() {
303                            Some(a_lower)
304                        } else {
305                            None
306                        }
307                    })
308                    .collect();
309
310                (None, req_attrs)
311            } else {
312                // What the client requested, in LDAP forms.
313                let req_attrs: Vec<String> = sr
314                    .attrs
315                    .iter()
316                    .filter_map(|a| {
317                        if a == "*" || a == "+" || a == "1.1" {
318                            None
319                        } else {
320                            Some(a.to_lowercase())
321                        }
322                    })
323                    .collect();
324                // This is what the client requested, but mapped to kanidm forms.
325                // NOTE: All req_attrs are lowercase at this point.
326                let mapped_attrs: BTreeSet<_> = req_attrs
327                    .iter()
328                    .map(|a| Attribute::from(ldap_vattr_map(a).unwrap_or(a)))
329                    .collect();
330
331                (Some(mapped_attrs), req_attrs)
332            };
333
334            admin_info!(attr = ?l_attrs, "LDAP Search Request LDAP Attrs");
335            admin_info!(attr = ?k_attrs, "LDAP Search Request Mapped Attrs");
336
337            let ct = duration_from_epoch_now();
338            let mut idm_read = idms.proxy_read().await?;
339            // Now start the txn - we need it for resolving filter components.
340
341            // join the filter, with ext_filter
342            let lfilter = match ext_filter {
343                Some(ext) => LdapFilter::And(vec![
344                    sr.filter.clone(),
345                    ext,
346                    LdapFilter::Not(Box::new(LdapFilter::Or(vec![
347                        LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
348                        LdapFilter::Equality(
349                            Attribute::Class.to_string(),
350                            "attributetype".to_string(),
351                        ),
352                        LdapFilter::Equality(
353                            Attribute::Class.to_string(),
354                            "access_control_profile".to_string(),
355                        ),
356                    ]))),
357                ]),
358                None => LdapFilter::And(vec![
359                    sr.filter.clone(),
360                    LdapFilter::Not(Box::new(LdapFilter::Or(vec![
361                        LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
362                        LdapFilter::Equality(
363                            Attribute::Class.to_string(),
364                            "attributetype".to_string(),
365                        ),
366                        LdapFilter::Equality(
367                            Attribute::Class.to_string(),
368                            "access_control_profile".to_string(),
369                        ),
370                    ]))),
371                ]),
372            };
373
374            admin_info!(filter = ?lfilter, "LDAP Search Filter");
375
376            // Build the event, with the permissions from effective_session
377            //
378            // ! Remember, searchEvent wraps to ignore hidden for us.
379            let ident = idm_read
380                .validate_ldap_session(&uat.effective_session, source, ct)
381                .map_err(|e| {
382                    admin_error!("Invalid identity: {:?}", e);
383                    e
384                })?;
385            let se = SearchEvent::new_ext_impersonate_uuid(
386                &mut idm_read.qs_read,
387                ident,
388                &lfilter,
389                k_attrs,
390            )
391            .map_err(|e| {
392                admin_error!("failed to create search event -> {:?}", e);
393                e
394            })?;
395
396            let res = idm_read.qs_read.search_ext(&se).map_err(|e| {
397                admin_error!("search failure {:?}", e);
398                e
399            })?;
400
401            // These have already been fully reduced (access controls applied),
402            // so we can just transform the values and open palm slam them into
403            // the result structure.
404            let lres: Result<Vec<_>, _> = res
405                .into_iter()
406                .map(|e| {
407                    e.to_ldap(
408                        &mut idm_read.qs_read,
409                        self.basedn.as_str(),
410                        all_attrs,
411                        &l_attrs,
412                    )
413                    // if okay, wrap in a ldap msg.
414                    .map(|r| sr.gen_result_entry(r))
415                })
416                .chain(iter::once(Ok(sr.gen_success())))
417                .collect();
418
419            let lres = lres.map_err(|e| {
420                admin_error!("entry resolve failure {:?}", e);
421                e
422            })?;
423
424            admin_info!(
425                nentries = %lres.len(),
426                "LDAP Search Success -> number of entries"
427            );
428
429            Ok(lres)
430        }
431    }
432
433    async fn do_bind(
434        &self,
435        idms: &IdmServer,
436        dn: &str,
437        pw: &str,
438    ) -> Result<Option<LdapBoundToken>, OperationError> {
439        security_info!(
440            "Attempt LDAP Bind for {}",
441            if dn.is_empty() { "(empty dn)" } else { dn }
442        );
443        let ct = duration_from_epoch_now();
444
445        let mut idm_auth = idms.auth().await?;
446        let target = self.bind_target_from_bind_dn(&mut idm_auth, dn, pw).await?;
447
448        let result = match target {
449            LdapBindTarget::Account(uuid) => {
450                let lae = LdapAuthEvent::from_parts(uuid, pw.to_string())?;
451                idm_auth.auth_ldap(&lae, ct).await?
452            }
453            LdapBindTarget::ApiToken => {
454                let jwsc = JwsCompact::from_str(pw).map_err(|err| {
455                    error!(?err, "Invalid JwsCompact supplied as authentication token.");
456                    OperationError::NotAuthenticated
457                })?;
458
459                let lae = LdapTokenAuthEvent::from_parts(jwsc)?;
460                idm_auth.token_auth_ldap(&lae, ct).await?
461            }
462            LdapBindTarget::Application(ref app_name, usr_uuid) => {
463                let lae =
464                    LdapApplicationAuthEvent::new(app_name.as_str(), usr_uuid, pw.to_string())?;
465                idm_auth.application_auth_ldap(&lae, ct).await?
466            }
467        };
468
469        idm_auth.commit()?;
470
471        if result.is_some() {
472            security_info!(
473                "✅ LDAP Bind success for {} -> {:?}",
474                if dn.is_empty() { "(empty dn)" } else { dn },
475                target
476            );
477        } else {
478            security_info!(
479                "❌ LDAP Bind failure for {} -> {:?}",
480                if dn.is_empty() { "(empty dn)" } else { dn },
481                target
482            );
483        }
484
485        Ok(result)
486    }
487
488    #[instrument(level = "debug", skip_all)]
489    async fn do_compare(
490        &self,
491        idms: &IdmServer,
492        cr: &CompareRequest,
493        uat: &LdapBoundToken,
494        source: Source,
495    ) -> Result<Vec<LdapMsg>, OperationError> {
496        admin_info!("Attempt LDAP CompareRequest for {}", uat.spn);
497
498        let (opt_attr, opt_value) = match self.dnre.captures(cr.entry.as_str()) {
499            Some(caps) => (
500                caps.name("attr").map(|v| v.as_str().to_string()),
501                caps.name("val").map(|v| v.as_str().to_string()),
502            ),
503            None => {
504                request_error!("LDAP Search failure - invalid basedn");
505                return Err(OperationError::InvalidRequestState);
506            }
507        };
508
509        let ext_filter = match (opt_attr, opt_value) {
510            (Some(a), Some(v)) => LdapFilter::Equality(a, v),
511            _ => {
512                request_error!("LDAP Search failure - invalid rdn");
513                return Err(OperationError::InvalidRequestState);
514            }
515        };
516
517        let ct = duration_from_epoch_now();
518        let mut idm_read = idms.proxy_read().await?;
519        // Now start the txn - we need it for resolving filter components.
520
521        // join the filter, with ext_filter
522        let lfilter = LdapFilter::And(vec![
523            ext_filter.clone(),
524            LdapFilter::Equality(cr.atype.clone(), cr.val.clone()),
525            LdapFilter::Not(Box::new(LdapFilter::Or(vec![
526                LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
527                LdapFilter::Equality(Attribute::Class.to_string(), "attributetype".to_string()),
528                LdapFilter::Equality(
529                    Attribute::Class.to_string(),
530                    "access_control_profile".to_string(),
531                ),
532            ]))),
533        ]);
534
535        admin_info!(filter = ?lfilter, "LDAP Compare Filter");
536
537        // Build the event, with the permissions from effective_session
538        let ident = idm_read
539            .validate_ldap_session(&uat.effective_session, source, ct)
540            .map_err(|e| {
541                admin_error!("Invalid identity: {:?}", e);
542                e
543            })?;
544
545        let f = Filter::from_ldap_ro(&ident, &lfilter, &mut idm_read.qs_read)?;
546        let filter_orig = f
547            .validate(idm_read.qs_read.get_schema())
548            .map_err(OperationError::SchemaViolation)?;
549        let filter = filter_orig.clone().into_ignore_hidden();
550
551        let ee = ExistsEvent {
552            ident: ident.clone(),
553            filter,
554            filter_orig,
555        };
556
557        let res = idm_read.qs_read.exists(&ee).map_err(|e| {
558            admin_error!("call to exists failure {:?}", e);
559            e
560        })?;
561
562        if res {
563            admin_info!("LDAP Compare -> True");
564            return Ok(vec![cr.gen_compare_true()]);
565        }
566
567        // we need to check if the entry exists at all (without the ava).
568        let lfilter = LdapFilter::And(vec![
569            ext_filter,
570            LdapFilter::Not(Box::new(LdapFilter::Or(vec![
571                LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
572                LdapFilter::Equality(Attribute::Class.to_string(), "attributetype".to_string()),
573                LdapFilter::Equality(
574                    Attribute::Class.to_string(),
575                    "access_control_profile".to_string(),
576                ),
577            ]))),
578        ]);
579        let f = Filter::from_ldap_ro(&ident, &lfilter, &mut idm_read.qs_read)?;
580        let filter_orig = f
581            .validate(idm_read.qs_read.get_schema())
582            .map_err(OperationError::SchemaViolation)?;
583        let filter = filter_orig.clone().into_ignore_hidden();
584        let ee = ExistsEvent {
585            ident,
586            filter,
587            filter_orig,
588        };
589
590        let res = idm_read.qs_read.exists(&ee).map_err(|e| {
591            admin_error!("call to exists failure {:?}", e);
592            e
593        })?;
594
595        if res {
596            admin_info!("LDAP Compare -> False");
597            return Ok(vec![cr.gen_compare_false()]);
598        }
599
600        Ok(vec![
601            cr.gen_error(LdapResultCode::NoSuchObject, "".to_string())
602        ])
603    }
604
605    pub async fn do_op(
606        &self,
607        idms: &IdmServer,
608        server_op: ServerOps,
609        uat: Option<LdapBoundToken>,
610        ip_addr: IpAddr,
611        eventid: Uuid,
612    ) -> Result<LdapResponseState, OperationError> {
613        let source = Source::Ldaps(ip_addr);
614
615        match server_op {
616            ServerOps::SimpleBind(sbr) => self
617                .do_bind(idms, sbr.dn.as_str(), sbr.pw.as_str())
618                .await
619                .map(|r| match r {
620                    Some(lbt) => LdapResponseState::Bind(lbt, sbr.gen_success()),
621                    None => LdapResponseState::Respond(sbr.gen_invalid_cred()),
622                })
623                .or_else(|e| {
624                    let (rc, msg) = operationerr_to_ldapresultcode(e);
625                    Ok(LdapResponseState::Respond(sbr.gen_error(rc, msg)))
626                }),
627            ServerOps::Search(sr) => match uat {
628                Some(u) => self
629                    .do_search(idms, &sr, &u, source)
630                    .await
631                    .map(LdapResponseState::MultiPartResponse)
632                    .or_else(|e| {
633                        let (rc, msg) = operationerr_to_ldapresultcode(e);
634                        Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)))
635                    }),
636                None => {
637                    // Search can occur without a bind, so bind first.
638                    // This is per section 4 of RFC 4513 (https://www.rfc-editor.org/rfc/rfc4513#section-4).
639                    let lbt = match self.do_bind(idms, "", "").await {
640                        Ok(Some(lbt)) => lbt,
641                        Ok(None) => {
642                            return Ok(LdapResponseState::Respond(
643                                sr.gen_error(LdapResultCode::InvalidCredentials, "".to_string()),
644                            ))
645                        }
646                        Err(e) => {
647                            let (rc, msg) = operationerr_to_ldapresultcode(e);
648                            return Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)));
649                        }
650                    };
651                    // If okay, do the search.
652                    self.do_search(idms, &sr, &lbt, Source::Internal)
653                        .await
654                        .map(|r| LdapResponseState::BindMultiPartResponse(lbt, r))
655                        .or_else(|e| {
656                            let (rc, msg) = operationerr_to_ldapresultcode(e);
657                            Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)))
658                        })
659                }
660            },
661            ServerOps::Unbind(_) => {
662                // No need to notify on unbind (per rfc4511)
663                Ok(LdapResponseState::Unbind)
664            }
665            ServerOps::Compare(cr) => match uat {
666                Some(u) => self
667                    .do_compare(idms, &cr, &u, source)
668                    .await
669                    .map(LdapResponseState::MultiPartResponse)
670                    .or_else(|e| {
671                        let (rc, msg) = operationerr_to_ldapresultcode(e);
672                        Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)))
673                    }),
674                None => {
675                    // Compare can occur without a bind, so bind first.
676                    // This is per section 4 of RFC 4513 (https://www.rfc-editor.org/rfc/rfc4513#section-4).
677                    let lbt = match self.do_bind(idms, "", "").await {
678                        Ok(Some(lbt)) => lbt,
679                        Ok(None) => {
680                            return Ok(LdapResponseState::Respond(
681                                cr.gen_error(LdapResultCode::InvalidCredentials, "".to_string()),
682                            ))
683                        }
684                        Err(e) => {
685                            let (rc, msg) = operationerr_to_ldapresultcode(e);
686                            return Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)));
687                        }
688                    };
689                    // If okay, do the compare.
690                    self.do_compare(idms, &cr, &lbt, Source::Internal)
691                        .await
692                        .map(|r| LdapResponseState::BindMultiPartResponse(lbt, r))
693                        .or_else(|e| {
694                            let (rc, msg) = operationerr_to_ldapresultcode(e);
695                            Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)))
696                        })
697                }
698            },
699            ServerOps::Whoami(wr) => match uat {
700                Some(u) => Ok(LdapResponseState::Respond(
701                    wr.gen_success(format!("u: {}", u.spn).as_str()),
702                )),
703                None => Ok(LdapResponseState::Respond(
704                    wr.gen_operror(format!("Unbound Connection {eventid}").as_str()),
705                )),
706            },
707        } // end match server op
708    }
709
710    async fn bind_target_from_bind_dn(
711        &self,
712        idm_auth: &mut IdmServerAuthTransaction<'_>,
713        dn: &str,
714        pw: &str,
715    ) -> Result<LdapBindTarget, OperationError> {
716        if dn.is_empty() {
717            if pw.is_empty() {
718                return Ok(LdapBindTarget::Account(UUID_ANONYMOUS));
719            } else {
720                // This is the path to access api-token logins.
721                return Ok(LdapBindTarget::ApiToken);
722            }
723        } else if dn == "dn=token" {
724            // Is the passed dn requesting token auth?
725            // We use dn= here since these are attr=value, and dn is a phantom so it will
726            // never be present or match a real value. We also make it an ava so that clients
727            // that over-zealously validate dn syntax are happy.
728            return Ok(LdapBindTarget::ApiToken);
729        }
730
731        if let Some(captures) = self.binddnre.captures(dn) {
732            if let Some(usr) = captures.name("val") {
733                let usr = usr.as_str();
734
735                if usr.is_empty() {
736                    error!("Failed to parse user name from bind DN, it is empty (capture group is {:#?})", captures.name("val"));
737                    return Err(OperationError::NoMatchingEntries);
738                }
739
740                let usr_uuid = idm_auth.qs_read.name_to_uuid(usr).map_err(|e| {
741                    error!(err = ?e, ?usr, "Error resolving rdn to target");
742                    e
743                })?;
744
745                if let Some(app) = captures.name("app") {
746                    let app = app.as_str();
747
748                    if app.is_empty() {
749                        error!("Failed to parse application name from bind DN, it is empty (capture group is {:#?})", captures.name("app"));
750                        return Err(OperationError::NoMatchingEntries);
751                    }
752
753                    return Ok(LdapBindTarget::Application(app.to_string(), usr_uuid));
754                }
755
756                return Ok(LdapBindTarget::Account(usr_uuid));
757            }
758        }
759
760        error!(
761            "Failed to parse bind DN, no captures. Bind DN was {:?})",
762            dn
763        );
764        Err(OperationError::NoMatchingEntries)
765    }
766}
767
768fn ldap_domain_to_dc(input: &str) -> String {
769    let mut output: String = String::new();
770    input.split('.').for_each(|dc| {
771        output.push_str("dc=");
772        output.push_str(dc);
773        #[allow(clippy::single_char_pattern, clippy::single_char_add_str)]
774        output.push_str(",");
775    });
776    // Remove the last ','
777    output.pop();
778    output
779}
780
781fn operationerr_to_ldapresultcode(e: OperationError) -> (LdapResultCode, String) {
782    match e {
783        OperationError::InvalidRequestState => {
784            (LdapResultCode::ConstraintViolation, "".to_string())
785        }
786        OperationError::InvalidAttributeName(s) | OperationError::InvalidAttribute(s) => {
787            (LdapResultCode::InvalidAttributeSyntax, s)
788        }
789        OperationError::SchemaViolation(se) => {
790            (LdapResultCode::UnwillingToPerform, format!("{se:?}"))
791        }
792        e => (LdapResultCode::Other, format!("{e:?}")),
793    }
794}
795
796#[inline]
797pub(crate) fn ldap_all_vattrs() -> Vec<String> {
798    vec![
799        ATTR_CN.to_string(),
800        ATTR_EMAIL.to_string(),
801        ATTR_LDAP_EMAIL_ADDRESS.to_string(),
802        LDAP_ATTR_DN.to_string(),
803        LDAP_ATTR_EMAIL_ALTERNATIVE.to_string(),
804        LDAP_ATTR_EMAIL_PRIMARY.to_string(),
805        LDAP_ATTR_ENTRYDN.to_string(),
806        LDAP_ATTR_ENTRYUUID.to_string(),
807        LDAP_ATTR_KEYS.to_string(),
808        LDAP_ATTR_MAIL_ALTERNATIVE.to_string(),
809        LDAP_ATTR_MAIL_PRIMARY.to_string(),
810        ATTR_OBJECTCLASS.to_string(),
811        ATTR_LDAP_SSHPUBLICKEY.to_string(),
812        ATTR_UIDNUMBER.to_string(),
813        ATTR_UID.to_string(),
814        ATTR_GECOS.to_string(),
815    ]
816}
817
818#[inline]
819pub(crate) fn ldap_vattr_map(input: &str) -> Option<&str> {
820    // ⚠️  WARNING ⚠️
821    // If you modify this list you MUST add these values to
822    // corresponding phantom attributes in the schema to prevent
823    // incorrect future or duplicate usage.
824    //
825    //   LDAP NAME     KANI ATTR SOURCE NAME
826    match input {
827        // EntryDN and DN have special handling in to_ldap in Entry. However, we
828        // need to map them to "name" so that if the user has requested dn/entrydn
829        // only, then we still requested at least one attribute from the backend
830        // allowing the access control tests to take place. Otherwise no entries
831        // would be returned.
832        ATTR_CN | ATTR_UID | LDAP_ATTR_ENTRYDN | LDAP_ATTR_DN => Some(ATTR_NAME),
833        ATTR_GECOS => Some(ATTR_DISPLAYNAME),
834        ATTR_EMAIL => Some(ATTR_MAIL),
835        ATTR_LDAP_EMAIL_ADDRESS => Some(ATTR_MAIL),
836        LDAP_ATTR_EMAIL_ALTERNATIVE => Some(ATTR_MAIL),
837        LDAP_ATTR_EMAIL_PRIMARY => Some(ATTR_MAIL),
838        LDAP_ATTR_ENTRYUUID => Some(ATTR_UUID),
839        LDAP_ATTR_KEYS => Some(ATTR_SSH_PUBLICKEY),
840        LDAP_ATTR_MAIL_ALTERNATIVE => Some(ATTR_MAIL),
841        LDAP_ATTR_MAIL_PRIMARY => Some(ATTR_MAIL),
842        ATTR_OBJECTCLASS => Some(ATTR_CLASS),
843        ATTR_LDAP_SSHPUBLICKEY => Some(ATTR_SSH_PUBLICKEY), // no-underscore -> underscore
844        ATTR_UIDNUMBER => Some(ATTR_GIDNUMBER),             // yes this is intentional
845        _ => None,
846    }
847}
848
849#[inline]
850pub(crate) fn ldap_attr_filter_map(input: &str) -> Attribute {
851    let a_lower = input.to_lowercase();
852    Attribute::from(ldap_vattr_map(&a_lower).unwrap_or(a_lower.as_str()))
853}
854
855#[cfg(test)]
856mod tests {
857    use crate::prelude::*;
858
859    use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
860    use hashbrown::HashSet;
861    use kanidm_proto::internal::ApiToken;
862    use ldap3_proto::proto::{
863        LdapFilter, LdapMsg, LdapOp, LdapResultCode, LdapSearchScope, LdapSubstringFilter,
864    };
865    use ldap3_proto::simple::*;
866
867    use super::{LdapServer, LdapSession};
868    use crate::idm::application::GenerateApplicationPasswordEvent;
869    use crate::idm::event::{LdapApplicationAuthEvent, UnixPasswordChangeEvent};
870    use crate::idm::serviceaccount::GenerateApiTokenEvent;
871
872    const TEST_PASSWORD: &str = "ntaoeuntnaoeuhraohuercahu😍";
873
874    #[idm_test]
875    async fn test_ldap_simple_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
876        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
877
878        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
879        // make the admin a valid posix account
880        let me_posix = ModifyEvent::new_internal_invalid(
881            filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
882            ModifyList::new_list(vec![
883                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
884                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
885            ]),
886        );
887        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
888
889        let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);
890
891        assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
892        assert!(idms_prox_write.commit().is_ok()); // Committing all configs
893
894        // default UNIX_PW bind (default is set to true)
895        // Hence allows all unix binds
896        let admin_t = ldaps
897            .do_bind(idms, "admin", TEST_PASSWORD)
898            .await
899            .unwrap()
900            .unwrap();
901        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
902        let admin_t = ldaps
903            .do_bind(idms, "admin@example.com", TEST_PASSWORD)
904            .await
905            .unwrap()
906            .unwrap();
907        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
908
909        // Setting UNIX_PW_BIND flag to false:
910        // Hence all of the below authentication will fail (asserts are still satisfied)
911        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
912        let disallow_unix_pw_flag = ModifyEvent::new_internal_invalid(
913            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
914            ModifyList::new_purge_and_set(Attribute::LdapAllowUnixPwBind, Value::Bool(false)),
915        );
916        assert!(idms_prox_write
917            .qs_write
918            .modify(&disallow_unix_pw_flag)
919            .is_ok());
920        assert!(idms_prox_write.commit().is_ok());
921        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
922        assert_eq!(
923            anon_t.effective_session,
924            LdapSession::UnixBind(UUID_ANONYMOUS)
925        );
926        assert!(
927            ldaps.do_bind(idms, "", "test").await.unwrap_err() == OperationError::NotAuthenticated
928        );
929        let admin_t = ldaps.do_bind(idms, "admin", TEST_PASSWORD).await.unwrap();
930        assert!(admin_t.is_none());
931
932        // Setting UNIX_PW_BIND flag to true :
933        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
934        let allow_unix_pw_flag = ModifyEvent::new_internal_invalid(
935            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
936            ModifyList::new_purge_and_set(Attribute::LdapAllowUnixPwBind, Value::Bool(true)),
937        );
938        assert!(idms_prox_write.qs_write.modify(&allow_unix_pw_flag).is_ok());
939        assert!(idms_prox_write.commit().is_ok());
940
941        // Now test the admin and various DN's
942        let admin_t = ldaps
943            .do_bind(idms, "admin", TEST_PASSWORD)
944            .await
945            .unwrap()
946            .unwrap();
947        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
948        let admin_t = ldaps
949            .do_bind(idms, "admin@example.com", TEST_PASSWORD)
950            .await
951            .unwrap()
952            .unwrap();
953        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
954        let admin_t = ldaps
955            .do_bind(idms, STR_UUID_ADMIN, TEST_PASSWORD)
956            .await
957            .unwrap()
958            .unwrap();
959        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
960        let admin_t = ldaps
961            .do_bind(idms, "name=admin,dc=example,dc=com", TEST_PASSWORD)
962            .await
963            .unwrap()
964            .unwrap();
965        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
966        let admin_t = ldaps
967            .do_bind(
968                idms,
969                "spn=admin@example.com,dc=example,dc=com",
970                TEST_PASSWORD,
971            )
972            .await
973            .unwrap()
974            .unwrap();
975        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
976        let admin_t = ldaps
977            .do_bind(
978                idms,
979                format!("uuid={STR_UUID_ADMIN},dc=example,dc=com").as_str(),
980                TEST_PASSWORD,
981            )
982            .await
983            .unwrap()
984            .unwrap();
985        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
986
987        let admin_t = ldaps
988            .do_bind(idms, "name=admin", TEST_PASSWORD)
989            .await
990            .unwrap()
991            .unwrap();
992        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
993        let admin_t = ldaps
994            .do_bind(idms, "spn=admin@example.com", TEST_PASSWORD)
995            .await
996            .unwrap()
997            .unwrap();
998        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
999        let admin_t = ldaps
1000            .do_bind(
1001                idms,
1002                format!("uuid={STR_UUID_ADMIN}").as_str(),
1003                TEST_PASSWORD,
1004            )
1005            .await
1006            .unwrap()
1007            .unwrap();
1008        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
1009
1010        let admin_t = ldaps
1011            .do_bind(idms, "admin,dc=example,dc=com", TEST_PASSWORD)
1012            .await
1013            .unwrap()
1014            .unwrap();
1015        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
1016        let admin_t = ldaps
1017            .do_bind(idms, "admin@example.com,dc=example,dc=com", TEST_PASSWORD)
1018            .await
1019            .unwrap()
1020            .unwrap();
1021        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
1022        let admin_t = ldaps
1023            .do_bind(
1024                idms,
1025                format!("{STR_UUID_ADMIN},dc=example,dc=com").as_str(),
1026                TEST_PASSWORD,
1027            )
1028            .await
1029            .unwrap()
1030            .unwrap();
1031        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
1032
1033        // Bad password, check last to prevent softlocking of the admin account.
1034        assert!(ldaps
1035            .do_bind(idms, "admin", "test")
1036            .await
1037            .unwrap()
1038            .is_none());
1039
1040        // Non-existent and invalid DNs
1041        assert!(ldaps
1042            .do_bind(
1043                idms,
1044                "spn=admin@example.com,dc=clownshoes,dc=example,dc=com",
1045                TEST_PASSWORD
1046            )
1047            .await
1048            .is_err());
1049        assert!(ldaps
1050            .do_bind(
1051                idms,
1052                "spn=claire@example.com,dc=example,dc=com",
1053                TEST_PASSWORD
1054            )
1055            .await
1056            .is_err());
1057        assert!(ldaps
1058            .do_bind(idms, ",dc=example,dc=com", TEST_PASSWORD)
1059            .await
1060            .is_err());
1061        assert!(ldaps
1062            .do_bind(idms, "dc=example,dc=com", TEST_PASSWORD)
1063            .await
1064            .is_err());
1065
1066        assert!(ldaps.do_bind(idms, "claire", "test").await.is_err());
1067    }
1068
1069    #[idm_test]
1070    async fn test_ldap_application_dnre(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
1071        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1072
1073        let testdn = format!("app=app1,{0}", ldaps.basedn);
1074        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
1075        assert!(captures.name("app").is_some());
1076        assert!(captures.name("attr").is_none());
1077        assert!(captures.name("val").is_none());
1078
1079        let testdn = format!("uid=foo,app=app1,{0}", ldaps.basedn);
1080        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
1081        assert!(captures.name("app").is_some());
1082        assert!(captures.name("attr").is_some());
1083        assert!(captures.name("val").is_some());
1084
1085        let testdn = format!("uid=foo,{0}", ldaps.basedn);
1086        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
1087        assert!(captures.name("app").is_none());
1088        assert!(captures.name("attr").is_some());
1089        assert!(captures.name("val").is_some());
1090    }
1091
1092    #[idm_test]
1093    async fn test_ldap_application_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
1094        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1095
1096        let usr_uuid = Uuid::new_v4();
1097        let grp_uuid = Uuid::new_v4();
1098        let app_uuid = Uuid::new_v4();
1099        let app_name = "testapp1";
1100
1101        // Setup person, group and application
1102        {
1103            let e1 = entry_init!(
1104                (Attribute::Class, EntryClass::Object.to_value()),
1105                (Attribute::Class, EntryClass::Account.to_value()),
1106                (Attribute::Class, EntryClass::Person.to_value()),
1107                (Attribute::Name, Value::new_iname("testperson1")),
1108                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1109                (Attribute::Description, Value::new_utf8s("testperson1")),
1110                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
1111            );
1112
1113            let e2 = entry_init!(
1114                (Attribute::Class, EntryClass::Object.to_value()),
1115                (Attribute::Class, EntryClass::Group.to_value()),
1116                (Attribute::Name, Value::new_iname("testgroup1")),
1117                (Attribute::Uuid, Value::Uuid(grp_uuid))
1118            );
1119
1120            let e3 = entry_init!(
1121                (Attribute::Class, EntryClass::Object.to_value()),
1122                (Attribute::Class, EntryClass::Account.to_value()),
1123                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1124                (Attribute::Class, EntryClass::Application.to_value()),
1125                (Attribute::DisplayName, Value::new_utf8s("Application")),
1126                (Attribute::Name, Value::new_iname(app_name)),
1127                (Attribute::Uuid, Value::Uuid(app_uuid)),
1128                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
1129            );
1130
1131            let ct = duration_from_epoch_now();
1132            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1133            assert!(server_txn
1134                .qs_write
1135                .internal_create(vec![e1, e2, e3])
1136                .and_then(|_| server_txn.commit())
1137                .is_ok());
1138        }
1139
1140        // Setup the anonymous login
1141        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
1142        assert_eq!(
1143            anon_t.effective_session,
1144            LdapSession::UnixBind(UUID_ANONYMOUS)
1145        );
1146
1147        // Searches under application base DN must show same content
1148        let sr = SearchRequest {
1149            msgid: 1,
1150            base: format!("app={app_name},dc=example,dc=com"),
1151            scope: LdapSearchScope::Subtree,
1152            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
1153            attrs: vec!["*".to_string()],
1154        };
1155
1156        let r1 = ldaps
1157            .do_search(idms, &sr, &anon_t, Source::Internal)
1158            .await
1159            .unwrap();
1160
1161        let sr = SearchRequest {
1162            msgid: 1,
1163            base: "dc=example,dc=com".to_string(),
1164            scope: LdapSearchScope::Subtree,
1165            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
1166            attrs: vec!["*".to_string()],
1167        };
1168
1169        let r2 = ldaps
1170            .do_search(idms, &sr, &anon_t, Source::Internal)
1171            .await
1172            .unwrap();
1173        assert!(!r1.is_empty());
1174        assert_eq!(r1.len(), r2.len());
1175    }
1176
1177    #[idm_test]
1178    async fn test_ldap_spn_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
1179        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1180
1181        let usr_uuid = Uuid::new_v4();
1182        let usr_name = "panko";
1183
1184        // Setup person, group and application
1185        {
1186            let e1: Entry<EntryInit, EntryNew> = entry_init!(
1187                (Attribute::Class, EntryClass::Object.to_value()),
1188                (Attribute::Class, EntryClass::Account.to_value()),
1189                (Attribute::Class, EntryClass::Person.to_value()),
1190                (Attribute::Name, Value::new_iname(usr_name)),
1191                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1192                (Attribute::DisplayName, Value::new_utf8s(usr_name))
1193            );
1194
1195            let ct = duration_from_epoch_now();
1196            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1197            assert!(server_txn
1198                .qs_write
1199                .internal_create(vec![e1])
1200                .and_then(|_| server_txn.commit())
1201                .is_ok());
1202        }
1203
1204        // Setup the anonymous login
1205        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
1206        assert_eq!(
1207            anon_t.effective_session,
1208            LdapSession::UnixBind(UUID_ANONYMOUS)
1209        );
1210
1211        // Searching a malformed spn shouldn't cause the query to fail
1212        let sr = SearchRequest {
1213            msgid: 1,
1214            base: "dc=example,dc=com".to_string(),
1215            scope: LdapSearchScope::Subtree,
1216            filter: LdapFilter::Or(vec![
1217                LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
1218                LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
1219            ]),
1220            attrs: vec!["*".to_string()],
1221        };
1222
1223        let result = ldaps
1224            .do_search(idms, &sr, &anon_t, Source::Internal)
1225            .await
1226            .map(|r| {
1227                r.into_iter()
1228                    .filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
1229                    .collect::<Vec<_>>()
1230            })
1231            .unwrap();
1232
1233        assert!(!result.is_empty());
1234
1235        let sr = SearchRequest {
1236            msgid: 1,
1237            base: "dc=example,dc=com".to_string(),
1238            scope: LdapSearchScope::Subtree,
1239            filter: LdapFilter::And(vec![
1240                LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
1241                LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
1242            ]),
1243            attrs: vec!["*".to_string()],
1244        };
1245
1246        let empty_result = ldaps
1247            .do_search(idms, &sr, &anon_t, Source::Internal)
1248            .await
1249            .map(|r| {
1250                r.into_iter()
1251                    .filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
1252                    .collect::<Vec<_>>()
1253            })
1254            .unwrap();
1255
1256        assert!(empty_result.is_empty());
1257    }
1258
1259    #[idm_test]
1260    async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
1261        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1262
1263        let usr_uuid = Uuid::new_v4();
1264        let grp_uuid = Uuid::new_v4();
1265        let app_uuid = Uuid::new_v4();
1266
1267        // Setup person, group and application
1268        {
1269            let e1 = entry_init!(
1270                (Attribute::Class, EntryClass::Object.to_value()),
1271                (Attribute::Class, EntryClass::Account.to_value()),
1272                (Attribute::Class, EntryClass::Person.to_value()),
1273                (Attribute::Name, Value::new_iname("testperson1")),
1274                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1275                (Attribute::Description, Value::new_utf8s("testperson1")),
1276                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
1277            );
1278
1279            let e2 = entry_init!(
1280                (Attribute::Class, EntryClass::Object.to_value()),
1281                (Attribute::Class, EntryClass::Group.to_value()),
1282                (Attribute::Name, Value::new_iname("testgroup1")),
1283                (Attribute::Uuid, Value::Uuid(grp_uuid))
1284            );
1285
1286            let e3 = entry_init!(
1287                (Attribute::Class, EntryClass::Object.to_value()),
1288                (Attribute::Class, EntryClass::Account.to_value()),
1289                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1290                (Attribute::Class, EntryClass::Application.to_value()),
1291                (Attribute::DisplayName, Value::new_utf8s("Application")),
1292                (Attribute::Name, Value::new_iname("testapp1")),
1293                (Attribute::Uuid, Value::Uuid(app_uuid)),
1294                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
1295            );
1296
1297            let ct = duration_from_epoch_now();
1298            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1299            assert!(server_txn
1300                .qs_write
1301                .internal_create(vec![e1, e2, e3])
1302                .and_then(|_| server_txn.commit())
1303                .is_ok());
1304        }
1305
1306        // No session, user not member of linked group
1307        let res = ldaps
1308            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
1309            .await;
1310        assert!(res.is_ok());
1311        assert!(res.unwrap().is_none());
1312
1313        {
1314            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
1315            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1316            assert!(idms_prox_write
1317                .qs_write
1318                .internal_modify_uuid(grp_uuid, &ml)
1319                .is_ok());
1320            assert!(idms_prox_write.commit().is_ok());
1321        }
1322
1323        // No session, user does not have app password for testapp1
1324        let res = ldaps
1325            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
1326            .await;
1327        assert!(res.is_ok());
1328        assert!(res.unwrap().is_none());
1329
1330        let pass1: String;
1331        let pass2: String;
1332        let pass3: String;
1333        {
1334            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1335
1336            let ev = GenerateApplicationPasswordEvent::new_internal(
1337                usr_uuid,
1338                app_uuid,
1339                "apppwd1".to_string(),
1340            );
1341            pass1 = idms_prox_write
1342                .generate_application_password(&ev)
1343                .expect("Failed to generate application password");
1344
1345            let ev = GenerateApplicationPasswordEvent::new_internal(
1346                usr_uuid,
1347                app_uuid,
1348                "apppwd2".to_string(),
1349            );
1350            pass2 = idms_prox_write
1351                .generate_application_password(&ev)
1352                .expect("Failed to generate application password");
1353
1354            assert!(idms_prox_write.commit().is_ok());
1355
1356            // Application password overwritten on duplicated label
1357            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1358            let ev = GenerateApplicationPasswordEvent::new_internal(
1359                usr_uuid,
1360                app_uuid,
1361                "apppwd2".to_string(),
1362            );
1363            pass3 = idms_prox_write
1364                .generate_application_password(&ev)
1365                .expect("Failed to generate application password");
1366            assert!(idms_prox_write.commit().is_ok());
1367        }
1368
1369        // Got session, app password valid
1370        let res = ldaps
1371            .do_bind(
1372                idms,
1373                "spn=testperson1,app=testapp1,dc=example,dc=com",
1374                pass1.as_str(),
1375            )
1376            .await;
1377        assert!(res.is_ok());
1378        assert!(res.unwrap().is_some());
1379
1380        // No session, app password overwritten
1381        let res = ldaps
1382            .do_bind(
1383                idms,
1384                "spn=testperson1,app=testapp1,dc=example,dc=com",
1385                pass2.as_str(),
1386            )
1387            .await;
1388        assert!(res.is_ok());
1389        assert!(res.unwrap().is_none());
1390
1391        // Got session, app password overwritten
1392        let res = ldaps
1393            .do_bind(
1394                idms,
1395                "spn=testperson1,app=testapp1,dc=example,dc=com",
1396                pass3.as_str(),
1397            )
1398            .await;
1399        assert!(res.is_ok());
1400        assert!(res.unwrap().is_some());
1401
1402        // No session, invalid app password
1403        let res = ldaps
1404            .do_bind(
1405                idms,
1406                "spn=testperson1,app=testapp1,dc=example,dc=com",
1407                "FOO",
1408            )
1409            .await;
1410        assert!(res.is_ok());
1411        assert!(res.unwrap().is_none());
1412    }
1413
1414    #[idm_test]
1415    async fn test_ldap_application_linked_group(
1416        idms: &IdmServer,
1417        _idms_delayed: &IdmServerDelayed,
1418    ) {
1419        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1420
1421        let usr_uuid = Uuid::new_v4();
1422        let usr_name = "testuser1";
1423
1424        let grp1_uuid = Uuid::new_v4();
1425        let grp1_name = "testgroup1";
1426        let grp2_uuid = Uuid::new_v4();
1427        let grp2_name = "testgroup2";
1428
1429        let app1_uuid = Uuid::new_v4();
1430        let app1_name = "testapp1";
1431        let app2_uuid = Uuid::new_v4();
1432        let app2_name = "testapp2";
1433
1434        // Setup person, groups and applications
1435        {
1436            let e1 = entry_init!(
1437                (Attribute::Class, EntryClass::Object.to_value()),
1438                (Attribute::Class, EntryClass::Account.to_value()),
1439                (Attribute::Class, EntryClass::Person.to_value()),
1440                (Attribute::Name, Value::new_iname(usr_name)),
1441                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1442                (Attribute::Description, Value::new_utf8s(usr_name)),
1443                (Attribute::DisplayName, Value::new_utf8s(usr_name))
1444            );
1445
1446            let e2 = entry_init!(
1447                (Attribute::Class, EntryClass::Object.to_value()),
1448                (Attribute::Class, EntryClass::Group.to_value()),
1449                (Attribute::Name, Value::new_iname(grp1_name)),
1450                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
1451                (Attribute::Member, Value::Refer(usr_uuid))
1452            );
1453
1454            let e3 = entry_init!(
1455                (Attribute::Class, EntryClass::Object.to_value()),
1456                (Attribute::Class, EntryClass::Group.to_value()),
1457                (Attribute::Name, Value::new_iname(grp2_name)),
1458                (Attribute::Uuid, Value::Uuid(grp2_uuid))
1459            );
1460
1461            let e4 = entry_init!(
1462                (Attribute::Class, EntryClass::Object.to_value()),
1463                (Attribute::Class, EntryClass::Account.to_value()),
1464                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1465                (Attribute::Class, EntryClass::Application.to_value()),
1466                (Attribute::DisplayName, Value::new_utf8s("Application")),
1467                (Attribute::Name, Value::new_iname(app1_name)),
1468                (Attribute::Uuid, Value::Uuid(app1_uuid)),
1469                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
1470            );
1471
1472            let e5 = entry_init!(
1473                (Attribute::Class, EntryClass::Object.to_value()),
1474                (Attribute::Class, EntryClass::Account.to_value()),
1475                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1476                (Attribute::Class, EntryClass::Application.to_value()),
1477                (Attribute::DisplayName, Value::new_utf8s("Application")),
1478                (Attribute::Name, Value::new_iname(app2_name)),
1479                (Attribute::Uuid, Value::Uuid(app2_uuid)),
1480                (Attribute::LinkedGroup, Value::Refer(grp2_uuid))
1481            );
1482
1483            let ct = duration_from_epoch_now();
1484            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1485            assert!(server_txn
1486                .qs_write
1487                .internal_create(vec![e1, e2, e3, e4, e5])
1488                .and_then(|_| server_txn.commit())
1489                .is_ok());
1490        }
1491
1492        let pass_app1: String;
1493        let pass_app2: String;
1494        {
1495            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1496
1497            let ev = GenerateApplicationPasswordEvent::new_internal(
1498                usr_uuid,
1499                app1_uuid,
1500                "label".to_string(),
1501            );
1502            pass_app1 = idms_prox_write
1503                .generate_application_password(&ev)
1504                .expect("Failed to generate application password");
1505
1506            // It is possible to generate an application password even if the
1507            // user is not member of the linked group
1508            let ev = GenerateApplicationPasswordEvent::new_internal(
1509                usr_uuid,
1510                app2_uuid,
1511                "label".to_string(),
1512            );
1513            pass_app2 = idms_prox_write
1514                .generate_application_password(&ev)
1515                .expect("Failed to generate application password");
1516
1517            assert!(idms_prox_write.commit().is_ok());
1518        }
1519
1520        // Got session, app password valid
1521        let res = ldaps
1522            .do_bind(
1523                idms,
1524                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
1525                pass_app1.as_str(),
1526            )
1527            .await;
1528        assert!(res.is_ok());
1529        assert!(res.unwrap().is_some());
1530
1531        // No session, not member
1532        let res = ldaps
1533            .do_bind(
1534                idms,
1535                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
1536                pass_app2.as_str(),
1537            )
1538            .await;
1539        assert!(res.is_ok());
1540        assert!(res.unwrap().is_none());
1541
1542        // Add user to grp2
1543        {
1544            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
1545            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1546            assert!(idms_prox_write
1547                .qs_write
1548                .internal_modify_uuid(grp2_uuid, &ml)
1549                .is_ok());
1550            assert!(idms_prox_write.commit().is_ok());
1551        }
1552
1553        // Got session, app password valid
1554        let res = ldaps
1555            .do_bind(
1556                idms,
1557                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
1558                pass_app2.as_str(),
1559            )
1560            .await;
1561        assert!(res.is_ok());
1562        assert!(res.unwrap().is_some());
1563
1564        // No session, wrong app
1565        let res = ldaps
1566            .do_bind(
1567                idms,
1568                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
1569                pass_app2.as_str(),
1570            )
1571            .await;
1572        assert!(res.is_ok());
1573        assert!(res.unwrap().is_none());
1574
1575        // Bind error, app not exists
1576        {
1577            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1578            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
1579                Attribute::Uuid,
1580                PartialValue::Uuid(app2_uuid)
1581            )));
1582            assert!(idms_prox_write.qs_write.delete(&de).is_ok());
1583            assert!(idms_prox_write.commit().is_ok());
1584        }
1585
1586        let res = ldaps
1587            .do_bind(
1588                idms,
1589                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
1590                pass_app2.as_str(),
1591            )
1592            .await;
1593        assert!(res.is_err());
1594    }
1595
1596    // For testing the timeouts
1597    // We need times on this scale
1598    //    not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
1599    const TEST_CURRENT_TIME: u64 = 6000;
1600    const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
1601    const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
1602    const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
1603    const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
1604
1605    async fn set_account_valid_time(idms: &IdmServer, acct: Uuid) {
1606        let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1607
1608        let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
1609        let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));
1610
1611        let me = ModifyEvent::new_internal_invalid(
1612            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(acct))),
1613            ModifyList::new_list(vec![
1614                Modify::Present(Attribute::AccountExpire, v_expire),
1615                Modify::Present(Attribute::AccountValidFrom, v_valid_from),
1616            ]),
1617        );
1618        assert!(idms_write.qs_write.modify(&me).is_ok());
1619        idms_write.commit().expect("Must not fail");
1620    }
1621
1622    #[idm_test]
1623    async fn test_ldap_application_valid_from_expire(
1624        idms: &IdmServer,
1625        _idms_delayed: &IdmServerDelayed,
1626    ) {
1627        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1628
1629        let usr_uuid = Uuid::new_v4();
1630        let usr_name = "testuser1";
1631
1632        let grp1_uuid = Uuid::new_v4();
1633        let grp1_name = "testgroup1";
1634
1635        let app1_uuid = Uuid::new_v4();
1636        let app1_name = "testapp1";
1637
1638        let pass_app1: String;
1639
1640        // Setup person, group, application and app password
1641        {
1642            let e1 = entry_init!(
1643                (Attribute::Class, EntryClass::Object.to_value()),
1644                (Attribute::Class, EntryClass::Account.to_value()),
1645                (Attribute::Class, EntryClass::Person.to_value()),
1646                (Attribute::Name, Value::new_iname(usr_name)),
1647                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1648                (Attribute::Description, Value::new_utf8s(usr_name)),
1649                (Attribute::DisplayName, Value::new_utf8s(usr_name))
1650            );
1651
1652            let e2 = entry_init!(
1653                (Attribute::Class, EntryClass::Object.to_value()),
1654                (Attribute::Class, EntryClass::Group.to_value()),
1655                (Attribute::Name, Value::new_iname(grp1_name)),
1656                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
1657                (Attribute::Member, Value::Refer(usr_uuid))
1658            );
1659
1660            let e3 = entry_init!(
1661                (Attribute::Class, EntryClass::Object.to_value()),
1662                (Attribute::Class, EntryClass::Account.to_value()),
1663                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1664                (Attribute::Class, EntryClass::Application.to_value()),
1665                (Attribute::DisplayName, Value::new_utf8s("Application")),
1666                (Attribute::Name, Value::new_iname(app1_name)),
1667                (Attribute::Uuid, Value::Uuid(app1_uuid)),
1668                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
1669            );
1670
1671            let ct = duration_from_epoch_now();
1672            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1673            assert!(server_txn
1674                .qs_write
1675                .internal_create(vec![e1, e2, e3])
1676                .and_then(|_| server_txn.commit())
1677                .is_ok());
1678
1679            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1680
1681            let ev = GenerateApplicationPasswordEvent::new_internal(
1682                usr_uuid,
1683                app1_uuid,
1684                "label".to_string(),
1685            );
1686            pass_app1 = idms_prox_write
1687                .generate_application_password(&ev)
1688                .expect("Failed to generate application password");
1689
1690            assert!(idms_prox_write.commit().is_ok());
1691        }
1692
1693        // Got session, app password valid
1694        let res = ldaps
1695            .do_bind(
1696                idms,
1697                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
1698                pass_app1.as_str(),
1699            )
1700            .await;
1701        assert!(res.is_ok());
1702        assert!(res.unwrap().is_some());
1703
1704        // Any account that is not yet valid / expired can't auth.
1705        // Set the valid bounds high/low
1706        // TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
1707        set_account_valid_time(idms, usr_uuid).await;
1708
1709        let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
1710        let time = Duration::from_secs(TEST_CURRENT_TIME);
1711        let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
1712
1713        let mut idms_auth = idms.auth().await.unwrap();
1714        let lae = LdapApplicationAuthEvent::new(app1_name, usr_uuid, pass_app1)
1715            .expect("Failed to build auth event");
1716
1717        let r1 = idms_auth
1718            .application_auth_ldap(&lae, time_low)
1719            .await
1720            .expect_err("Authentication succeeded");
1721        assert_eq!(r1, OperationError::SessionExpired);
1722
1723        let r1 = idms_auth
1724            .application_auth_ldap(&lae, time)
1725            .await
1726            .expect("Failed auth");
1727        assert!(r1.is_some());
1728
1729        let r1 = idms_auth
1730            .application_auth_ldap(&lae, time_high)
1731            .await
1732            .expect_err("Authentication succeeded");
1733        assert_eq!(r1, OperationError::SessionExpired);
1734    }
1735
1736    macro_rules! assert_entry_contains {
1737        (
1738            $entry:expr,
1739            $dn:expr,
1740            $($item:expr),*
1741        ) => {{
1742            assert_eq!($entry.dn, $dn);
1743            // Build a set from the attrs.
1744            let mut attrs = HashSet::new();
1745            for a in $entry.attributes.iter() {
1746                for v in a.vals.iter() {
1747                    attrs.insert((a.atype.as_str(), v.as_slice()));
1748                }
1749            };
1750            info!(?attrs);
1751            $(
1752                warn!("{}", $item.0);
1753                assert!(attrs.contains(&(
1754                    $item.0.as_ref(), $item.1.as_bytes()
1755                )));
1756            )*
1757
1758        }};
1759    }
1760
1761    #[idm_test]
1762    async fn test_ldap_virtual_attribute_generation(
1763        idms: &IdmServer,
1764        _idms_delayed: &IdmServerDelayed,
1765    ) {
1766        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1767
1768        let ssh_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst";
1769
1770        // Setup a user we want to check.
1771        {
1772            let e1 = entry_init!(
1773                (Attribute::Class, EntryClass::Object.to_value()),
1774                (Attribute::Class, EntryClass::Person.to_value()),
1775                (Attribute::Class, EntryClass::Account.to_value()),
1776                (Attribute::Class, EntryClass::PosixAccount.to_value()),
1777                (Attribute::Name, Value::new_iname("testperson1")),
1778                (
1779                    Attribute::Uuid,
1780                    Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
1781                ),
1782                (Attribute::Description, Value::new_utf8s("testperson1")),
1783                (Attribute::DisplayName, Value::new_utf8s("testperson1")),
1784                (Attribute::GidNumber, Value::new_uint32(12345)),
1785                (Attribute::LoginShell, Value::new_iutf8("/bin/zsh")),
1786                (
1787                    Attribute::SshPublicKey,
1788                    Value::new_sshkey_str("test", ssh_ed25519).expect("Invalid ssh key")
1789                )
1790            );
1791
1792            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1793            let ce = CreateEvent::new_internal(vec![e1]);
1794            assert!(server_txn
1795                .qs_write
1796                .create(&ce)
1797                .and_then(|_| server_txn.commit())
1798                .is_ok());
1799        }
1800
1801        // Setup the anonymous login.
1802        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
1803        assert_eq!(
1804            anon_t.effective_session,
1805            LdapSession::UnixBind(UUID_ANONYMOUS)
1806        );
1807
1808        // Check that when we request *, we get default list.
1809        let sr = SearchRequest {
1810            msgid: 1,
1811            base: "dc=example,dc=com".to_string(),
1812            scope: LdapSearchScope::Subtree,
1813            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1814            attrs: vec!["*".to_string()],
1815        };
1816        let r1 = ldaps
1817            .do_search(idms, &sr, &anon_t, Source::Internal)
1818            .await
1819            .unwrap();
1820
1821        // The result, and the ldap proto success msg.
1822        assert_eq!(r1.len(), 2);
1823        match &r1[0].op {
1824            LdapOp::SearchResultEntry(lsre) => {
1825                assert_entry_contains!(
1826                    lsre,
1827                    "spn=testperson1@example.com,dc=example,dc=com",
1828                    (Attribute::Class, EntryClass::Object.to_string()),
1829                    (Attribute::Class, EntryClass::Person.to_string()),
1830                    (Attribute::Class, EntryClass::Account.to_string()),
1831                    (Attribute::Class, EntryClass::PosixAccount.to_string()),
1832                    (Attribute::DisplayName, "testperson1"),
1833                    (Attribute::Name, "testperson1"),
1834                    (Attribute::GidNumber, "12345"),
1835                    (Attribute::LoginShell, "/bin/zsh"),
1836                    (Attribute::SshPublicKey, ssh_ed25519),
1837                    (Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
1838                );
1839            }
1840            _ => panic!("Oh no"),
1841        };
1842
1843        // Check that when we request +, we get all attrs and the vattrs
1844        let sr = SearchRequest {
1845            msgid: 1,
1846            base: "dc=example,dc=com".to_string(),
1847            scope: LdapSearchScope::Subtree,
1848            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1849            attrs: vec!["+".to_string()],
1850        };
1851        let r1 = ldaps
1852            .do_search(idms, &sr, &anon_t, Source::Internal)
1853            .await
1854            .unwrap();
1855
1856        // The result, and the ldap proto success msg.
1857        assert_eq!(r1.len(), 2);
1858        match &r1[0].op {
1859            LdapOp::SearchResultEntry(lsre) => {
1860                assert_entry_contains!(
1861                    lsre,
1862                    "spn=testperson1@example.com,dc=example,dc=com",
1863                    (Attribute::ObjectClass, EntryClass::Object.as_ref()),
1864                    (Attribute::ObjectClass, EntryClass::Person.as_ref()),
1865                    (Attribute::ObjectClass, EntryClass::Account.as_ref()),
1866                    (Attribute::ObjectClass, EntryClass::PosixAccount.as_ref()),
1867                    (Attribute::DisplayName, "testperson1"),
1868                    (Attribute::Name, "testperson1"),
1869                    (Attribute::GidNumber, "12345"),
1870                    (Attribute::LoginShell, "/bin/zsh"),
1871                    (Attribute::SshPublicKey, ssh_ed25519),
1872                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
1873                    (
1874                        Attribute::EntryDn,
1875                        "spn=testperson1@example.com,dc=example,dc=com"
1876                    ),
1877                    (Attribute::UidNumber, "12345"),
1878                    (Attribute::Cn, "testperson1"),
1879                    (Attribute::LdapKeys, ssh_ed25519)
1880                );
1881            }
1882            _ => panic!("Oh no"),
1883        };
1884
1885        // Check that when we request an attr by name, we get all of them correctly.
1886        let sr = SearchRequest {
1887            msgid: 1,
1888            base: "dc=example,dc=com".to_string(),
1889            scope: LdapSearchScope::Subtree,
1890            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1891            attrs: vec![
1892                LDAP_ATTR_NAME.to_string(),
1893                Attribute::EntryDn.to_string(),
1894                ATTR_LDAP_KEYS.to_string(),
1895                Attribute::UidNumber.to_string(),
1896            ],
1897        };
1898        let r1 = ldaps
1899            .do_search(idms, &sr, &anon_t, Source::Internal)
1900            .await
1901            .unwrap();
1902
1903        // The result, and the ldap proto success msg.
1904        assert_eq!(r1.len(), 2);
1905        match &r1[0].op {
1906            LdapOp::SearchResultEntry(lsre) => {
1907                assert_entry_contains!(
1908                    lsre,
1909                    "spn=testperson1@example.com,dc=example,dc=com",
1910                    (Attribute::Name, "testperson1"),
1911                    (
1912                        Attribute::EntryDn,
1913                        "spn=testperson1@example.com,dc=example,dc=com"
1914                    ),
1915                    (Attribute::UidNumber, "12345"),
1916                    (Attribute::LdapKeys, ssh_ed25519)
1917                );
1918            }
1919            _ => panic!("Oh no"),
1920        };
1921    }
1922
1923    #[idm_test]
1924    async fn test_ldap_token_privilege_granting(
1925        idms: &IdmServer,
1926        _idms_delayed: &IdmServerDelayed,
1927    ) {
1928        // Setup the ldap server
1929        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1930
1931        // Prebuild the search req we'll be using this test.
1932        let sr = SearchRequest {
1933            msgid: 1,
1934            base: "dc=example,dc=com".to_string(),
1935            scope: LdapSearchScope::Subtree,
1936            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1937            attrs: vec![
1938                LDAP_ATTR_NAME,
1939                LDAP_ATTR_MAIL,
1940                LDAP_ATTR_MAIL_PRIMARY,
1941                LDAP_ATTR_MAIL_ALTERNATIVE,
1942                LDAP_ATTR_EMAIL_PRIMARY,
1943                LDAP_ATTR_EMAIL_ALTERNATIVE,
1944            ]
1945            .into_iter()
1946            .map(|s| s.to_string())
1947            .collect(),
1948        };
1949
1950        let sa_uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
1951
1952        // Configure the user account that will have the tokens issued.
1953        // Should be a SERVICE account.
1954        let apitoken = {
1955            // Create a service account,
1956
1957            let e1 = entry_init!(
1958                (Attribute::Class, EntryClass::Object.to_value()),
1959                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1960                (Attribute::Class, EntryClass::Account.to_value()),
1961                (Attribute::Uuid, Value::Uuid(sa_uuid)),
1962                (Attribute::Name, Value::new_iname("service_permission_test")),
1963                (
1964                    Attribute::DisplayName,
1965                    Value::new_utf8s("service_permission_test")
1966                )
1967            );
1968
1969            // Setup a person with an email
1970            let e2 = entry_init!(
1971                (Attribute::Class, EntryClass::Object.to_value()),
1972                (Attribute::Class, EntryClass::Person.to_value()),
1973                (Attribute::Class, EntryClass::Account.to_value()),
1974                (Attribute::Class, EntryClass::PosixAccount.to_value()),
1975                (Attribute::Name, Value::new_iname("testperson1")),
1976                (
1977                    Attribute::Mail,
1978                    Value::EmailAddress("testperson1@example.com".to_string(), true)
1979                ),
1980                (
1981                    Attribute::Mail,
1982                    Value::EmailAddress("testperson1.alternative@example.com".to_string(), false)
1983                ),
1984                (Attribute::Description, Value::new_utf8s("testperson1")),
1985                (Attribute::DisplayName, Value::new_utf8s("testperson1")),
1986                (Attribute::GidNumber, Value::new_uint32(12345)),
1987                (Attribute::LoginShell, Value::new_iutf8("/bin/zsh"))
1988            );
1989
1990            // Setup an access control for the service account to view mail attrs.
1991
1992            let ct = duration_from_epoch_now();
1993
1994            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1995            let ce = CreateEvent::new_internal(vec![e1, e2]);
1996            assert!(server_txn.qs_write.create(&ce).is_ok());
1997
1998            // idm_people_read_priv
1999            let me = ModifyEvent::new_internal_invalid(
2000                filter!(f_eq(
2001                    Attribute::Name,
2002                    PartialValue::new_iname("idm_people_pii_read")
2003                )),
2004                ModifyList::new_list(vec![Modify::Present(
2005                    Attribute::Member,
2006                    Value::Refer(sa_uuid),
2007                )]),
2008            );
2009            assert!(server_txn.qs_write.modify(&me).is_ok());
2010
2011            // Issue a token
2012            // make it purpose = ldap <- currently purpose isn't supported,
2013            // it's an idea for future.
2014            let gte = GenerateApiTokenEvent::new_internal(sa_uuid, "TestToken", None);
2015
2016            let apitoken = server_txn
2017                .service_account_generate_api_token(&gte, ct)
2018                .expect("Failed to create new apitoken");
2019
2020            assert!(server_txn.commit().is_ok());
2021
2022            apitoken
2023        };
2024
2025        // assert the token fails on non-ldap events token-xchg <- currently
2026        // we don't have purpose so this isn't tested.
2027
2028        // Bind with anonymous, search and show mail attr isn't accessible.
2029        let anon_lbt = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2030        assert_eq!(
2031            anon_lbt.effective_session,
2032            LdapSession::UnixBind(UUID_ANONYMOUS)
2033        );
2034
2035        let r1 = ldaps
2036            .do_search(idms, &sr, &anon_lbt, Source::Internal)
2037            .await
2038            .unwrap();
2039        assert_eq!(r1.len(), 2);
2040        match &r1[0].op {
2041            LdapOp::SearchResultEntry(lsre) => {
2042                assert_entry_contains!(
2043                    lsre,
2044                    "spn=testperson1@example.com,dc=example,dc=com",
2045                    (Attribute::Name, "testperson1")
2046                );
2047            }
2048            _ => panic!("Oh no"),
2049        };
2050
2051        // Inspect the token to get its uuid out.
2052        let jws_verifier = JwsDangerReleaseWithoutVerify::default();
2053
2054        let apitoken_inner = jws_verifier
2055            .verify(&apitoken)
2056            .unwrap()
2057            .from_json::<ApiToken>()
2058            .unwrap();
2059
2060        // Bind using the token as a DN
2061        let sa_lbt = ldaps
2062            .do_bind(idms, "dn=token", &apitoken.to_string())
2063            .await
2064            .unwrap()
2065            .unwrap();
2066        assert_eq!(
2067            sa_lbt.effective_session,
2068            LdapSession::ApiToken(apitoken_inner.clone())
2069        );
2070
2071        // Bind using the token as a pw
2072        let sa_lbt = ldaps
2073            .do_bind(idms, "", &apitoken.to_string())
2074            .await
2075            .unwrap()
2076            .unwrap();
2077        assert_eq!(
2078            sa_lbt.effective_session,
2079            LdapSession::ApiToken(apitoken_inner)
2080        );
2081
2082        // Search and retrieve mail that's now accessible.
2083        let r1 = ldaps
2084            .do_search(idms, &sr, &sa_lbt, Source::Internal)
2085            .await
2086            .unwrap();
2087        assert_eq!(r1.len(), 2);
2088        match &r1[0].op {
2089            LdapOp::SearchResultEntry(lsre) => {
2090                assert_entry_contains!(
2091                    lsre,
2092                    "spn=testperson1@example.com,dc=example,dc=com",
2093                    (Attribute::Name, "testperson1"),
2094                    (Attribute::Mail, "testperson1@example.com"),
2095                    (Attribute::Mail, "testperson1.alternative@example.com"),
2096                    (LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
2097                    (
2098                        LDAP_ATTR_MAIL_ALTERNATIVE,
2099                        "testperson1.alternative@example.com"
2100                    ),
2101                    (LDAP_ATTR_EMAIL_PRIMARY, "testperson1@example.com"),
2102                    (
2103                        LDAP_ATTR_EMAIL_ALTERNATIVE,
2104                        "testperson1.alternative@example.com"
2105                    )
2106                );
2107            }
2108            _ => panic!("Oh no"),
2109        };
2110
2111        // ======= test with a substring search
2112
2113        let sr = SearchRequest {
2114            msgid: 2,
2115            base: "dc=example,dc=com".to_string(),
2116            scope: LdapSearchScope::Subtree,
2117            filter: LdapFilter::And(vec![
2118                LdapFilter::Equality(Attribute::Class.to_string(), "posixAccount".to_string()),
2119                LdapFilter::Substring(
2120                    LDAP_ATTR_MAIL.to_string(),
2121                    LdapSubstringFilter {
2122                        initial: None,
2123                        any: vec![],
2124                        final_: Some("@example.com".to_string()),
2125                    },
2126                ),
2127            ]),
2128            attrs: vec![
2129                LDAP_ATTR_NAME,
2130                LDAP_ATTR_MAIL,
2131                LDAP_ATTR_MAIL_PRIMARY,
2132                LDAP_ATTR_MAIL_ALTERNATIVE,
2133            ]
2134            .into_iter()
2135            .map(|s| s.to_string())
2136            .collect(),
2137        };
2138
2139        let r1 = ldaps
2140            .do_search(idms, &sr, &sa_lbt, Source::Internal)
2141            .await
2142            .unwrap();
2143
2144        assert_eq!(r1.len(), 2);
2145        match &r1[0].op {
2146            LdapOp::SearchResultEntry(lsre) => {
2147                assert_entry_contains!(
2148                    lsre,
2149                    "spn=testperson1@example.com,dc=example,dc=com",
2150                    (Attribute::Name, "testperson1"),
2151                    (Attribute::Mail, "testperson1@example.com"),
2152                    (Attribute::Mail, "testperson1.alternative@example.com"),
2153                    (LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
2154                    (
2155                        LDAP_ATTR_MAIL_ALTERNATIVE,
2156                        "testperson1.alternative@example.com"
2157                    )
2158                );
2159            }
2160            _ => panic!("Oh no"),
2161        };
2162    }
2163
2164    #[idm_test]
2165    async fn test_ldap_virtual_attribute_with_all_attr_search(
2166        idms: &IdmServer,
2167        _idms_delayed: &IdmServerDelayed,
2168    ) {
2169        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2170
2171        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2172
2173        // Setup a user we want to check.
2174        {
2175            let e1 = entry_init!(
2176                (Attribute::Class, EntryClass::Person.to_value()),
2177                (Attribute::Class, EntryClass::Account.to_value()),
2178                (Attribute::Name, Value::new_iname("testperson1")),
2179                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2180                (Attribute::Description, Value::new_utf8s("testperson1")),
2181                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2182            );
2183
2184            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2185            assert!(server_txn
2186                .qs_write
2187                .internal_create(vec![e1])
2188                .and_then(|_| server_txn.commit())
2189                .is_ok());
2190        }
2191
2192        // Setup the anonymous login.
2193        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2194        assert_eq!(
2195            anon_t.effective_session,
2196            LdapSession::UnixBind(UUID_ANONYMOUS)
2197        );
2198
2199        // Check that when we request a virtual attr by name *and* all_attrs we get all the requested values.
2200        let sr = SearchRequest {
2201            msgid: 1,
2202            base: "dc=example,dc=com".to_string(),
2203            scope: LdapSearchScope::Subtree,
2204            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2205            attrs: vec![
2206                "*".to_string(),
2207                // Already being returned
2208                LDAP_ATTR_NAME.to_string(),
2209                // This is a virtual attribute
2210                Attribute::EntryUuid.to_string(),
2211            ],
2212        };
2213        let r1 = ldaps
2214            .do_search(idms, &sr, &anon_t, Source::Internal)
2215            .await
2216            .unwrap();
2217
2218        // The result, and the ldap proto success msg.
2219        assert_eq!(r1.len(), 2);
2220        match &r1[0].op {
2221            LdapOp::SearchResultEntry(lsre) => {
2222                assert_entry_contains!(
2223                    lsre,
2224                    "spn=testperson1@example.com,dc=example,dc=com",
2225                    (Attribute::Name, "testperson1"),
2226                    (Attribute::DisplayName, "testperson1"),
2227                    (Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
2228                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
2229                );
2230            }
2231            _ => panic!("Oh no"),
2232        };
2233    }
2234
2235    // Test behaviour of the 1.1 attribute.
2236    #[idm_test]
2237    async fn test_ldap_one_dot_one_attribute(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2238        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2239
2240        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2241
2242        // Setup a user we want to check.
2243        {
2244            let e1 = entry_init!(
2245                (Attribute::Class, EntryClass::Person.to_value()),
2246                (Attribute::Class, EntryClass::Account.to_value()),
2247                (Attribute::Name, Value::new_iname("testperson1")),
2248                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2249                (Attribute::Description, Value::new_utf8s("testperson1")),
2250                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2251            );
2252
2253            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2254            assert!(server_txn
2255                .qs_write
2256                .internal_create(vec![e1])
2257                .and_then(|_| server_txn.commit())
2258                .is_ok());
2259        }
2260
2261        // Setup the anonymous login.
2262        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2263        assert_eq!(
2264            anon_t.effective_session,
2265            LdapSession::UnixBind(UUID_ANONYMOUS)
2266        );
2267
2268        // If we request only 1.1, we get no attributes.
2269        let sr = SearchRequest {
2270            msgid: 1,
2271            base: "dc=example,dc=com".to_string(),
2272            scope: LdapSearchScope::Subtree,
2273            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2274            attrs: vec!["1.1".to_string()],
2275        };
2276        let r1 = ldaps
2277            .do_search(idms, &sr, &anon_t, Source::Internal)
2278            .await
2279            .unwrap();
2280
2281        // The result, and the ldap proto success msg.
2282        assert_eq!(r1.len(), 2);
2283        match &r1[0].op {
2284            LdapOp::SearchResultEntry(lsre) => {
2285                assert_eq!(
2286                    lsre.dn.as_str(),
2287                    "spn=testperson1@example.com,dc=example,dc=com"
2288                );
2289                assert!(lsre.attributes.is_empty());
2290            }
2291            _ => panic!("Oh no"),
2292        };
2293
2294        // If we request 1.1 and another attr, 1.1 is IGNORED.
2295        let sr = SearchRequest {
2296            msgid: 1,
2297            base: "dc=example,dc=com".to_string(),
2298            scope: LdapSearchScope::Subtree,
2299            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2300            attrs: vec![
2301                "1.1".to_string(),
2302                // This should be present.
2303                Attribute::EntryUuid.to_string(),
2304            ],
2305        };
2306        let r1 = ldaps
2307            .do_search(idms, &sr, &anon_t, Source::Internal)
2308            .await
2309            .unwrap();
2310
2311        // The result, and the ldap proto success msg.
2312        assert_eq!(r1.len(), 2);
2313        match &r1[0].op {
2314            LdapOp::SearchResultEntry(lsre) => {
2315                assert_entry_contains!(
2316                    lsre,
2317                    "spn=testperson1@example.com,dc=example,dc=com",
2318                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
2319                );
2320            }
2321            _ => panic!("Oh no"),
2322        };
2323    }
2324
2325    #[idm_test]
2326    async fn test_ldap_rootdse_basedn_change(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2327        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2328
2329        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2330        assert_eq!(
2331            anon_t.effective_session,
2332            LdapSession::UnixBind(UUID_ANONYMOUS)
2333        );
2334
2335        let sr = SearchRequest {
2336            msgid: 1,
2337            base: "".to_string(),
2338            scope: LdapSearchScope::Base,
2339            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
2340            attrs: vec!["*".to_string()],
2341        };
2342        let r1 = ldaps
2343            .do_search(idms, &sr, &anon_t, Source::Internal)
2344            .await
2345            .unwrap();
2346
2347        trace!(?r1);
2348
2349        // The result, and the ldap proto success msg.
2350        assert_eq!(r1.len(), 2);
2351        match &r1[0].op {
2352            LdapOp::SearchResultEntry(lsre) => {
2353                assert_entry_contains!(
2354                    lsre,
2355                    "",
2356                    (Attribute::ObjectClass, "top"),
2357                    ("vendorname", "Kanidm Project"),
2358                    ("supportedldapversion", "3"),
2359                    ("defaultnamingcontext", "dc=example,dc=com")
2360                );
2361            }
2362            _ => panic!("Oh no"),
2363        };
2364
2365        drop(ldaps);
2366
2367        // Change the domain basedn
2368
2369        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2370        // make the admin a valid posix account
2371        let me_posix = ModifyEvent::new_internal_invalid(
2372            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
2373            ModifyList::new_purge_and_set(
2374                Attribute::DomainLdapBasedn,
2375                Value::new_iutf8("o=kanidmproject"),
2376            ),
2377        );
2378        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
2379
2380        assert!(idms_prox_write.commit().is_ok());
2381
2382        // Now re-test
2383        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2384
2385        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2386        assert_eq!(
2387            anon_t.effective_session,
2388            LdapSession::UnixBind(UUID_ANONYMOUS)
2389        );
2390
2391        let sr = SearchRequest {
2392            msgid: 1,
2393            base: "".to_string(),
2394            scope: LdapSearchScope::Base,
2395            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
2396            attrs: vec!["*".to_string()],
2397        };
2398        let r1 = ldaps
2399            .do_search(idms, &sr, &anon_t, Source::Internal)
2400            .await
2401            .unwrap();
2402
2403        trace!(?r1);
2404
2405        // The result, and the ldap proto success msg.
2406        assert_eq!(r1.len(), 2);
2407        match &r1[0].op {
2408            LdapOp::SearchResultEntry(lsre) => {
2409                assert_entry_contains!(
2410                    lsre,
2411                    "",
2412                    (Attribute::ObjectClass, "top"),
2413                    ("vendorname", "Kanidm Project"),
2414                    ("supportedldapversion", "3"),
2415                    ("defaultnamingcontext", "o=kanidmproject")
2416                );
2417            }
2418            _ => panic!("Oh no"),
2419        };
2420    }
2421
2422    #[idm_test]
2423    async fn test_ldap_sssd_compat(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2424        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2425
2426        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2427
2428        // Setup a user we want to check.
2429        {
2430            let e1 = entry_init!(
2431                (Attribute::Class, EntryClass::Person.to_value()),
2432                (Attribute::Class, EntryClass::Account.to_value()),
2433                (Attribute::Class, EntryClass::PosixAccount.to_value()),
2434                (Attribute::Name, Value::new_iname("testperson1")),
2435                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2436                (Attribute::GidNumber, Value::Uint32(12345)),
2437                (Attribute::Description, Value::new_utf8s("testperson1")),
2438                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2439            );
2440
2441            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2442            assert!(server_txn
2443                .qs_write
2444                .internal_create(vec![e1])
2445                .and_then(|_| server_txn.commit())
2446                .is_ok());
2447        }
2448
2449        // Setup the anonymous login.
2450        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2451        assert_eq!(
2452            anon_t.effective_session,
2453            LdapSession::UnixBind(UUID_ANONYMOUS)
2454        );
2455
2456        // SSSD tries to just search for silly attrs all the time. We ignore them.
2457        let sr = SearchRequest {
2458            msgid: 1,
2459            base: "dc=example,dc=com".to_string(),
2460            scope: LdapSearchScope::Subtree,
2461            filter: LdapFilter::And(vec![
2462                LdapFilter::Equality(Attribute::Class.to_string(), "sudohost".to_string()),
2463                LdapFilter::Substring(
2464                    Attribute::SudoHost.to_string(),
2465                    LdapSubstringFilter {
2466                        initial: Some("a".to_string()),
2467                        any: vec!["x".to_string()],
2468                        final_: Some("z".to_string()),
2469                    },
2470                ),
2471            ]),
2472            attrs: vec![
2473                "*".to_string(),
2474                // Already being returned
2475                LDAP_ATTR_NAME.to_string(),
2476                // This is a virtual attribute
2477                Attribute::EntryUuid.to_string(),
2478            ],
2479        };
2480        let r1 = ldaps
2481            .do_search(idms, &sr, &anon_t, Source::Internal)
2482            .await
2483            .unwrap();
2484
2485        // Empty results and ldap proto success msg.
2486        assert_eq!(r1.len(), 1);
2487
2488        // Second search
2489
2490        let sr = SearchRequest {
2491            msgid: 1,
2492            base: "dc=example,dc=com".to_string(),
2493            scope: LdapSearchScope::Subtree,
2494            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2495            attrs: vec![
2496                "uid".to_string(),
2497                "uidNumber".to_string(),
2498                "gidNumber".to_string(),
2499                "gecos".to_string(),
2500                "cn".to_string(),
2501                "entryuuid".to_string(),
2502            ],
2503        };
2504        let r1 = ldaps
2505            .do_search(idms, &sr, &anon_t, Source::Internal)
2506            .await
2507            .unwrap();
2508
2509        trace!(?r1);
2510
2511        // The result, and the ldap proto success msg.
2512        assert_eq!(r1.len(), 2);
2513        match &r1[0].op {
2514            LdapOp::SearchResultEntry(lsre) => {
2515                assert_entry_contains!(
2516                    lsre,
2517                    "spn=testperson1@example.com,dc=example,dc=com",
2518                    (Attribute::Uid, "testperson1"),
2519                    (Attribute::Cn, "testperson1"),
2520                    (Attribute::Gecos, "testperson1"),
2521                    (Attribute::UidNumber, "12345"),
2522                    (Attribute::GidNumber, "12345"),
2523                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
2524                );
2525            }
2526            _ => panic!("Oh no"),
2527        };
2528    }
2529
2530    #[idm_test]
2531    async fn test_ldap_compare_request(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2532        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2533
2534        // Setup a user we want to check.
2535        {
2536            let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2537
2538            let e1 = entry_init!(
2539                (Attribute::Class, EntryClass::Person.to_value()),
2540                (Attribute::Class, EntryClass::Account.to_value()),
2541                (Attribute::Class, EntryClass::PosixAccount.to_value()),
2542                (Attribute::Name, Value::new_iname("testperson1")),
2543                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2544                (Attribute::GidNumber, Value::Uint32(12345)),
2545                (Attribute::Description, Value::new_utf8s("testperson1")),
2546                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2547            );
2548
2549            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2550            assert!(server_txn
2551                .qs_write
2552                .internal_create(vec![e1])
2553                .and_then(|_| server_txn.commit())
2554                .is_ok());
2555        }
2556
2557        // Setup the anonymous login.
2558        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2559        assert_eq!(
2560            anon_t.effective_session,
2561            LdapSession::UnixBind(UUID_ANONYMOUS)
2562        );
2563
2564        #[track_caller]
2565        fn assert_compare_result(r: &[LdapMsg], code: &LdapResultCode) {
2566            assert_eq!(r.len(), 1);
2567            match &r[0].op {
2568                LdapOp::CompareResult(lcr) => {
2569                    assert_eq!(&lcr.code, code);
2570                }
2571                _ => panic!("Oh no"),
2572            };
2573        }
2574
2575        let cr = CompareRequest {
2576            msgid: 1,
2577            entry: "name=testperson1,dc=example,dc=com".to_string(),
2578            atype: Attribute::Name.to_string(),
2579            val: "testperson1".to_string(),
2580        };
2581
2582        assert_compare_result(
2583            &ldaps
2584                .do_compare(idms, &cr, &anon_t, Source::Internal)
2585                .await
2586                .unwrap(),
2587            &LdapResultCode::CompareTrue,
2588        );
2589
2590        let cr = CompareRequest {
2591            msgid: 1,
2592            entry: "name=testperson1,dc=example,dc=com".to_string(),
2593            atype: Attribute::GidNumber.to_string(),
2594            val: "12345".to_string(),
2595        };
2596
2597        assert_compare_result(
2598            &ldaps
2599                .do_compare(idms, &cr, &anon_t, Source::Internal)
2600                .await
2601                .unwrap(),
2602            &LdapResultCode::CompareTrue,
2603        );
2604
2605        let cr = CompareRequest {
2606            msgid: 1,
2607            entry: "name=testperson1,dc=example,dc=com".to_string(),
2608            atype: Attribute::Name.to_string(),
2609            val: "other".to_string(),
2610        };
2611        assert_compare_result(
2612            &ldaps
2613                .do_compare(idms, &cr, &anon_t, Source::Internal)
2614                .await
2615                .unwrap(),
2616            &LdapResultCode::CompareFalse,
2617        );
2618
2619        let cr = CompareRequest {
2620            msgid: 1,
2621            entry: "name=other,dc=example,dc=com".to_string(),
2622            atype: Attribute::Name.to_string(),
2623            val: "other".to_string(),
2624        };
2625        assert_compare_result(
2626            &ldaps
2627                .do_compare(idms, &cr, &anon_t, Source::Internal)
2628                .await
2629                .unwrap(),
2630            &LdapResultCode::NoSuchObject,
2631        );
2632
2633        let cr = CompareRequest {
2634            msgid: 1,
2635            entry: "invalidentry".to_string(),
2636            atype: Attribute::Name.to_string(),
2637            val: "other".to_string(),
2638        };
2639        assert!(&ldaps
2640            .do_compare(idms, &cr, &anon_t, Source::Internal)
2641            .await
2642            .is_err());
2643
2644        let cr = CompareRequest {
2645            msgid: 1,
2646            entry: "name=other,dc=example,dc=com".to_string(),
2647            atype: "invalid".to_string(),
2648            val: "other".to_string(),
2649        };
2650        assert_eq!(
2651            &ldaps
2652                .do_compare(idms, &cr, &anon_t, Source::Internal)
2653                .await
2654                .unwrap_err(),
2655            &OperationError::InvalidAttributeName("invalid".to_string()),
2656        );
2657    }
2658
2659    #[idm_test]
2660    async fn test_ldap_maximum_queryable_attributes(
2661        idms: &IdmServer,
2662        _idms_delayed: &IdmServerDelayed,
2663    ) {
2664        // Set the max queryable attrs to 2
2665
2666        let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2667
2668        let set_ldap_maximum_queryable_attrs = ModifyEvent::new_internal_invalid(
2669            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
2670            ModifyList::new_purge_and_set(Attribute::LdapMaxQueryableAttrs, Value::Uint32(2)),
2671        );
2672        assert!(server_txn
2673            .qs_write
2674            .modify(&set_ldap_maximum_queryable_attrs)
2675            .and_then(|_| server_txn.commit())
2676            .is_ok());
2677
2678        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2679
2680        let usr_uuid = Uuid::new_v4();
2681        let grp_uuid = Uuid::new_v4();
2682        let app_uuid = Uuid::new_v4();
2683        let app_name = "testapp1";
2684
2685        // Setup person, group and application
2686        {
2687            let e1 = entry_init!(
2688                (Attribute::Class, EntryClass::Object.to_value()),
2689                (Attribute::Class, EntryClass::Account.to_value()),
2690                (Attribute::Class, EntryClass::Person.to_value()),
2691                (Attribute::Name, Value::new_iname("testperson1")),
2692                (Attribute::Uuid, Value::Uuid(usr_uuid)),
2693                (Attribute::Description, Value::new_utf8s("testperson1")),
2694                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2695            );
2696
2697            let e2 = entry_init!(
2698                (Attribute::Class, EntryClass::Object.to_value()),
2699                (Attribute::Class, EntryClass::Group.to_value()),
2700                (Attribute::Name, Value::new_iname("testgroup1")),
2701                (Attribute::Uuid, Value::Uuid(grp_uuid))
2702            );
2703
2704            let e3 = entry_init!(
2705                (Attribute::Class, EntryClass::Object.to_value()),
2706                (Attribute::Class, EntryClass::Account.to_value()),
2707                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
2708                (Attribute::Class, EntryClass::Application.to_value()),
2709                (Attribute::DisplayName, Value::new_utf8s("Application")),
2710                (Attribute::Name, Value::new_iname(app_name)),
2711                (Attribute::Uuid, Value::Uuid(app_uuid)),
2712                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
2713            );
2714
2715            let ct = duration_from_epoch_now();
2716            let mut server_txn = idms.proxy_write(ct).await.unwrap();
2717            assert!(server_txn
2718                .qs_write
2719                .internal_create(vec![e1, e2, e3])
2720                .and_then(|_| server_txn.commit())
2721                .is_ok());
2722        }
2723
2724        // Setup the anonymous login
2725        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2726        assert_eq!(
2727            anon_t.effective_session,
2728            LdapSession::UnixBind(UUID_ANONYMOUS)
2729        );
2730
2731        let invalid_search = SearchRequest {
2732            msgid: 1,
2733            base: "dc=example,dc=com".to_string(),
2734            scope: LdapSearchScope::Subtree,
2735            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
2736            attrs: vec![
2737                "objectClass".to_string(),
2738                "cn".to_string(),
2739                "givenName".to_string(),
2740            ],
2741        };
2742
2743        let valid_search = SearchRequest {
2744            msgid: 1,
2745            base: "dc=example,dc=com".to_string(),
2746            scope: LdapSearchScope::Subtree,
2747            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
2748            attrs: vec!["objectClass: person".to_string()],
2749        };
2750
2751        let invalid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
2752            .do_search(idms, &invalid_search, &anon_t, Source::Internal)
2753            .await;
2754
2755        let valid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
2756            .do_search(idms, &valid_search, &anon_t, Source::Internal)
2757            .await;
2758
2759        assert_eq!(invalid_res, Err(OperationError::ResourceLimit));
2760        assert!(valid_res.is_ok());
2761    }
2762}