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::ServiceAccount.to_value()),
1123                (Attribute::Class, EntryClass::Application.to_value()),
1124                (Attribute::Name, Value::new_iname(app_name)),
1125                (Attribute::Uuid, Value::Uuid(app_uuid)),
1126                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
1127            );
1128
1129            let ct = duration_from_epoch_now();
1130            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1131            assert!(server_txn
1132                .qs_write
1133                .internal_create(vec![e1, e2, e3])
1134                .and_then(|_| server_txn.commit())
1135                .is_ok());
1136        }
1137
1138        // Setup the anonymous login
1139        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
1140        assert_eq!(
1141            anon_t.effective_session,
1142            LdapSession::UnixBind(UUID_ANONYMOUS)
1143        );
1144
1145        // Searches under application base DN must show same content
1146        let sr = SearchRequest {
1147            msgid: 1,
1148            base: format!("app={app_name},dc=example,dc=com"),
1149            scope: LdapSearchScope::Subtree,
1150            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
1151            attrs: vec!["*".to_string()],
1152        };
1153
1154        let r1 = ldaps
1155            .do_search(idms, &sr, &anon_t, Source::Internal)
1156            .await
1157            .unwrap();
1158
1159        let sr = SearchRequest {
1160            msgid: 1,
1161            base: "dc=example,dc=com".to_string(),
1162            scope: LdapSearchScope::Subtree,
1163            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
1164            attrs: vec!["*".to_string()],
1165        };
1166
1167        let r2 = ldaps
1168            .do_search(idms, &sr, &anon_t, Source::Internal)
1169            .await
1170            .unwrap();
1171        assert!(!r1.is_empty());
1172        assert_eq!(r1.len(), r2.len());
1173    }
1174
1175    #[idm_test]
1176    async fn test_ldap_spn_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
1177        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1178
1179        let usr_uuid = Uuid::new_v4();
1180        let usr_name = "panko";
1181
1182        // Setup person, group and application
1183        {
1184            let e1: Entry<EntryInit, EntryNew> = entry_init!(
1185                (Attribute::Class, EntryClass::Object.to_value()),
1186                (Attribute::Class, EntryClass::Account.to_value()),
1187                (Attribute::Class, EntryClass::Person.to_value()),
1188                (Attribute::Name, Value::new_iname(usr_name)),
1189                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1190                (Attribute::DisplayName, Value::new_utf8s(usr_name))
1191            );
1192
1193            let ct = duration_from_epoch_now();
1194            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1195            assert!(server_txn
1196                .qs_write
1197                .internal_create(vec![e1])
1198                .and_then(|_| server_txn.commit())
1199                .is_ok());
1200        }
1201
1202        // Setup the anonymous login
1203        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
1204        assert_eq!(
1205            anon_t.effective_session,
1206            LdapSession::UnixBind(UUID_ANONYMOUS)
1207        );
1208
1209        // Searching a malformed spn shouldn't cause the query to fail
1210        let sr = SearchRequest {
1211            msgid: 1,
1212            base: "dc=example,dc=com".to_string(),
1213            scope: LdapSearchScope::Subtree,
1214            filter: LdapFilter::Or(vec![
1215                LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
1216                LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
1217            ]),
1218            attrs: vec!["*".to_string()],
1219        };
1220
1221        let result = ldaps
1222            .do_search(idms, &sr, &anon_t, Source::Internal)
1223            .await
1224            .map(|r| {
1225                r.into_iter()
1226                    .filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
1227                    .collect::<Vec<_>>()
1228            })
1229            .unwrap();
1230
1231        assert!(!result.is_empty());
1232
1233        let sr = SearchRequest {
1234            msgid: 1,
1235            base: "dc=example,dc=com".to_string(),
1236            scope: LdapSearchScope::Subtree,
1237            filter: LdapFilter::And(vec![
1238                LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
1239                LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
1240            ]),
1241            attrs: vec!["*".to_string()],
1242        };
1243
1244        let empty_result = ldaps
1245            .do_search(idms, &sr, &anon_t, Source::Internal)
1246            .await
1247            .map(|r| {
1248                r.into_iter()
1249                    .filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
1250                    .collect::<Vec<_>>()
1251            })
1252            .unwrap();
1253
1254        assert!(empty_result.is_empty());
1255    }
1256
1257    #[idm_test]
1258    async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
1259        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1260
1261        let usr_uuid = Uuid::new_v4();
1262        let grp_uuid = Uuid::new_v4();
1263        let app_uuid = Uuid::new_v4();
1264
1265        // Setup person, group and application
1266        {
1267            let e1 = entry_init!(
1268                (Attribute::Class, EntryClass::Object.to_value()),
1269                (Attribute::Class, EntryClass::Account.to_value()),
1270                (Attribute::Class, EntryClass::Person.to_value()),
1271                (Attribute::Name, Value::new_iname("testperson1")),
1272                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1273                (Attribute::Description, Value::new_utf8s("testperson1")),
1274                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
1275            );
1276
1277            let e2 = entry_init!(
1278                (Attribute::Class, EntryClass::Object.to_value()),
1279                (Attribute::Class, EntryClass::Group.to_value()),
1280                (Attribute::Name, Value::new_iname("testgroup1")),
1281                (Attribute::Uuid, Value::Uuid(grp_uuid))
1282            );
1283
1284            let e3 = entry_init!(
1285                (Attribute::Class, EntryClass::Object.to_value()),
1286                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1287                (Attribute::Class, EntryClass::Application.to_value()),
1288                (Attribute::Name, Value::new_iname("testapp1")),
1289                (Attribute::Uuid, Value::Uuid(app_uuid)),
1290                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
1291            );
1292
1293            let ct = duration_from_epoch_now();
1294            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1295            assert!(server_txn
1296                .qs_write
1297                .internal_create(vec![e1, e2, e3])
1298                .and_then(|_| server_txn.commit())
1299                .is_ok());
1300        }
1301
1302        // No session, user not member of linked group
1303        let res = ldaps
1304            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
1305            .await;
1306        assert!(res.is_ok());
1307        assert!(res.unwrap().is_none());
1308
1309        {
1310            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
1311            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1312            assert!(idms_prox_write
1313                .qs_write
1314                .internal_modify_uuid(grp_uuid, &ml)
1315                .is_ok());
1316            assert!(idms_prox_write.commit().is_ok());
1317        }
1318
1319        // No session, user does not have app password for testapp1
1320        let res = ldaps
1321            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
1322            .await;
1323        assert!(res.is_ok());
1324        assert!(res.unwrap().is_none());
1325
1326        let pass1: String;
1327        let pass2: String;
1328        let pass3: String;
1329        {
1330            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1331
1332            let ev = GenerateApplicationPasswordEvent::new_internal(
1333                usr_uuid,
1334                app_uuid,
1335                "apppwd1".to_string(),
1336            );
1337            pass1 = idms_prox_write
1338                .generate_application_password(&ev)
1339                .expect("Failed to generate application password");
1340
1341            let ev = GenerateApplicationPasswordEvent::new_internal(
1342                usr_uuid,
1343                app_uuid,
1344                "apppwd2".to_string(),
1345            );
1346            pass2 = idms_prox_write
1347                .generate_application_password(&ev)
1348                .expect("Failed to generate application password");
1349
1350            assert!(idms_prox_write.commit().is_ok());
1351
1352            // Application password overwritten on duplicated label
1353            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1354            let ev = GenerateApplicationPasswordEvent::new_internal(
1355                usr_uuid,
1356                app_uuid,
1357                "apppwd2".to_string(),
1358            );
1359            pass3 = idms_prox_write
1360                .generate_application_password(&ev)
1361                .expect("Failed to generate application password");
1362            assert!(idms_prox_write.commit().is_ok());
1363        }
1364
1365        // Got session, app password valid
1366        let res = ldaps
1367            .do_bind(
1368                idms,
1369                "spn=testperson1,app=testapp1,dc=example,dc=com",
1370                pass1.as_str(),
1371            )
1372            .await;
1373        assert!(res.is_ok());
1374        assert!(res.unwrap().is_some());
1375
1376        // No session, app password overwritten
1377        let res = ldaps
1378            .do_bind(
1379                idms,
1380                "spn=testperson1,app=testapp1,dc=example,dc=com",
1381                pass2.as_str(),
1382            )
1383            .await;
1384        assert!(res.is_ok());
1385        assert!(res.unwrap().is_none());
1386
1387        // Got session, app password overwritten
1388        let res = ldaps
1389            .do_bind(
1390                idms,
1391                "spn=testperson1,app=testapp1,dc=example,dc=com",
1392                pass3.as_str(),
1393            )
1394            .await;
1395        assert!(res.is_ok());
1396        assert!(res.unwrap().is_some());
1397
1398        // No session, invalid app password
1399        let res = ldaps
1400            .do_bind(
1401                idms,
1402                "spn=testperson1,app=testapp1,dc=example,dc=com",
1403                "FOO",
1404            )
1405            .await;
1406        assert!(res.is_ok());
1407        assert!(res.unwrap().is_none());
1408    }
1409
1410    #[idm_test]
1411    async fn test_ldap_application_linked_group(
1412        idms: &IdmServer,
1413        _idms_delayed: &IdmServerDelayed,
1414    ) {
1415        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1416
1417        let usr_uuid = Uuid::new_v4();
1418        let usr_name = "testuser1";
1419
1420        let grp1_uuid = Uuid::new_v4();
1421        let grp1_name = "testgroup1";
1422        let grp2_uuid = Uuid::new_v4();
1423        let grp2_name = "testgroup2";
1424
1425        let app1_uuid = Uuid::new_v4();
1426        let app1_name = "testapp1";
1427        let app2_uuid = Uuid::new_v4();
1428        let app2_name = "testapp2";
1429
1430        // Setup person, groups and applications
1431        {
1432            let e1 = entry_init!(
1433                (Attribute::Class, EntryClass::Object.to_value()),
1434                (Attribute::Class, EntryClass::Account.to_value()),
1435                (Attribute::Class, EntryClass::Person.to_value()),
1436                (Attribute::Name, Value::new_iname(usr_name)),
1437                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1438                (Attribute::Description, Value::new_utf8s(usr_name)),
1439                (Attribute::DisplayName, Value::new_utf8s(usr_name))
1440            );
1441
1442            let e2 = entry_init!(
1443                (Attribute::Class, EntryClass::Object.to_value()),
1444                (Attribute::Class, EntryClass::Group.to_value()),
1445                (Attribute::Name, Value::new_iname(grp1_name)),
1446                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
1447                (Attribute::Member, Value::Refer(usr_uuid))
1448            );
1449
1450            let e3 = entry_init!(
1451                (Attribute::Class, EntryClass::Object.to_value()),
1452                (Attribute::Class, EntryClass::Group.to_value()),
1453                (Attribute::Name, Value::new_iname(grp2_name)),
1454                (Attribute::Uuid, Value::Uuid(grp2_uuid))
1455            );
1456
1457            let e4 = entry_init!(
1458                (Attribute::Class, EntryClass::Object.to_value()),
1459                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1460                (Attribute::Class, EntryClass::Application.to_value()),
1461                (Attribute::Name, Value::new_iname(app1_name)),
1462                (Attribute::Uuid, Value::Uuid(app1_uuid)),
1463                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
1464            );
1465
1466            let e5 = entry_init!(
1467                (Attribute::Class, EntryClass::Object.to_value()),
1468                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1469                (Attribute::Class, EntryClass::Application.to_value()),
1470                (Attribute::Name, Value::new_iname(app2_name)),
1471                (Attribute::Uuid, Value::Uuid(app2_uuid)),
1472                (Attribute::LinkedGroup, Value::Refer(grp2_uuid))
1473            );
1474
1475            let ct = duration_from_epoch_now();
1476            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1477            assert!(server_txn
1478                .qs_write
1479                .internal_create(vec![e1, e2, e3, e4, e5])
1480                .and_then(|_| server_txn.commit())
1481                .is_ok());
1482        }
1483
1484        let pass_app1: String;
1485        let pass_app2: String;
1486        {
1487            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1488
1489            let ev = GenerateApplicationPasswordEvent::new_internal(
1490                usr_uuid,
1491                app1_uuid,
1492                "label".to_string(),
1493            );
1494            pass_app1 = idms_prox_write
1495                .generate_application_password(&ev)
1496                .expect("Failed to generate application password");
1497
1498            // It is possible to generate an application password even if the
1499            // user is not member of the linked group
1500            let ev = GenerateApplicationPasswordEvent::new_internal(
1501                usr_uuid,
1502                app2_uuid,
1503                "label".to_string(),
1504            );
1505            pass_app2 = idms_prox_write
1506                .generate_application_password(&ev)
1507                .expect("Failed to generate application password");
1508
1509            assert!(idms_prox_write.commit().is_ok());
1510        }
1511
1512        // Got session, app password valid
1513        let res = ldaps
1514            .do_bind(
1515                idms,
1516                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
1517                pass_app1.as_str(),
1518            )
1519            .await;
1520        assert!(res.is_ok());
1521        assert!(res.unwrap().is_some());
1522
1523        // No session, not member
1524        let res = ldaps
1525            .do_bind(
1526                idms,
1527                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
1528                pass_app2.as_str(),
1529            )
1530            .await;
1531        assert!(res.is_ok());
1532        assert!(res.unwrap().is_none());
1533
1534        // Add user to grp2
1535        {
1536            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
1537            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1538            assert!(idms_prox_write
1539                .qs_write
1540                .internal_modify_uuid(grp2_uuid, &ml)
1541                .is_ok());
1542            assert!(idms_prox_write.commit().is_ok());
1543        }
1544
1545        // Got session, app password valid
1546        let res = ldaps
1547            .do_bind(
1548                idms,
1549                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
1550                pass_app2.as_str(),
1551            )
1552            .await;
1553        assert!(res.is_ok());
1554        assert!(res.unwrap().is_some());
1555
1556        // No session, wrong app
1557        let res = ldaps
1558            .do_bind(
1559                idms,
1560                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
1561                pass_app2.as_str(),
1562            )
1563            .await;
1564        assert!(res.is_ok());
1565        assert!(res.unwrap().is_none());
1566
1567        // Bind error, app not exists
1568        {
1569            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1570            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
1571                Attribute::Uuid,
1572                PartialValue::Uuid(app2_uuid)
1573            )));
1574            assert!(idms_prox_write.qs_write.delete(&de).is_ok());
1575            assert!(idms_prox_write.commit().is_ok());
1576        }
1577
1578        let res = ldaps
1579            .do_bind(
1580                idms,
1581                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
1582                pass_app2.as_str(),
1583            )
1584            .await;
1585        assert!(res.is_err());
1586    }
1587
1588    // For testing the timeouts
1589    // We need times on this scale
1590    //    not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
1591    const TEST_CURRENT_TIME: u64 = 6000;
1592    const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
1593    const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
1594    const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
1595    const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
1596
1597    async fn set_account_valid_time(idms: &IdmServer, acct: Uuid) {
1598        let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1599
1600        let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
1601        let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));
1602
1603        let me = ModifyEvent::new_internal_invalid(
1604            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(acct))),
1605            ModifyList::new_list(vec![
1606                Modify::Present(Attribute::AccountExpire, v_expire),
1607                Modify::Present(Attribute::AccountValidFrom, v_valid_from),
1608            ]),
1609        );
1610        assert!(idms_write.qs_write.modify(&me).is_ok());
1611        idms_write.commit().expect("Must not fail");
1612    }
1613
1614    #[idm_test]
1615    async fn test_ldap_application_valid_from_expire(
1616        idms: &IdmServer,
1617        _idms_delayed: &IdmServerDelayed,
1618    ) {
1619        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1620
1621        let usr_uuid = Uuid::new_v4();
1622        let usr_name = "testuser1";
1623
1624        let grp1_uuid = Uuid::new_v4();
1625        let grp1_name = "testgroup1";
1626
1627        let app1_uuid = Uuid::new_v4();
1628        let app1_name = "testapp1";
1629
1630        let pass_app1: String;
1631
1632        // Setup person, group, application and app password
1633        {
1634            let e1 = entry_init!(
1635                (Attribute::Class, EntryClass::Object.to_value()),
1636                (Attribute::Class, EntryClass::Account.to_value()),
1637                (Attribute::Class, EntryClass::Person.to_value()),
1638                (Attribute::Name, Value::new_iname(usr_name)),
1639                (Attribute::Uuid, Value::Uuid(usr_uuid)),
1640                (Attribute::Description, Value::new_utf8s(usr_name)),
1641                (Attribute::DisplayName, Value::new_utf8s(usr_name))
1642            );
1643
1644            let e2 = entry_init!(
1645                (Attribute::Class, EntryClass::Object.to_value()),
1646                (Attribute::Class, EntryClass::Group.to_value()),
1647                (Attribute::Name, Value::new_iname(grp1_name)),
1648                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
1649                (Attribute::Member, Value::Refer(usr_uuid))
1650            );
1651
1652            let e3 = entry_init!(
1653                (Attribute::Class, EntryClass::Object.to_value()),
1654                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1655                (Attribute::Class, EntryClass::Application.to_value()),
1656                (Attribute::Name, Value::new_iname(app1_name)),
1657                (Attribute::Uuid, Value::Uuid(app1_uuid)),
1658                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
1659            );
1660
1661            let ct = duration_from_epoch_now();
1662            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1663            assert!(server_txn
1664                .qs_write
1665                .internal_create(vec![e1, e2, e3])
1666                .and_then(|_| server_txn.commit())
1667                .is_ok());
1668
1669            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1670
1671            let ev = GenerateApplicationPasswordEvent::new_internal(
1672                usr_uuid,
1673                app1_uuid,
1674                "label".to_string(),
1675            );
1676            pass_app1 = idms_prox_write
1677                .generate_application_password(&ev)
1678                .expect("Failed to generate application password");
1679
1680            assert!(idms_prox_write.commit().is_ok());
1681        }
1682
1683        // Got session, app password valid
1684        let res = ldaps
1685            .do_bind(
1686                idms,
1687                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
1688                pass_app1.as_str(),
1689            )
1690            .await;
1691        assert!(res.is_ok());
1692        assert!(res.unwrap().is_some());
1693
1694        // Any account that is not yet valid / expired can't auth.
1695        // Set the valid bounds high/low
1696        // TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
1697        set_account_valid_time(idms, usr_uuid).await;
1698
1699        let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
1700        let time = Duration::from_secs(TEST_CURRENT_TIME);
1701        let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
1702
1703        let mut idms_auth = idms.auth().await.unwrap();
1704        let lae = LdapApplicationAuthEvent::new(app1_name, usr_uuid, pass_app1)
1705            .expect("Failed to build auth event");
1706
1707        let r1 = idms_auth
1708            .application_auth_ldap(&lae, time_low)
1709            .await
1710            .expect_err("Authentication succeeded");
1711        assert_eq!(r1, OperationError::SessionExpired);
1712
1713        let r1 = idms_auth
1714            .application_auth_ldap(&lae, time)
1715            .await
1716            .expect("Failed auth");
1717        assert!(r1.is_some());
1718
1719        let r1 = idms_auth
1720            .application_auth_ldap(&lae, time_high)
1721            .await
1722            .expect_err("Authentication succeeded");
1723        assert_eq!(r1, OperationError::SessionExpired);
1724    }
1725
1726    macro_rules! assert_entry_contains {
1727        (
1728            $entry:expr,
1729            $dn:expr,
1730            $($item:expr),*
1731        ) => {{
1732            assert_eq!($entry.dn, $dn);
1733            // Build a set from the attrs.
1734            let mut attrs = HashSet::new();
1735            for a in $entry.attributes.iter() {
1736                for v in a.vals.iter() {
1737                    attrs.insert((a.atype.as_str(), v.as_slice()));
1738                }
1739            };
1740            info!(?attrs);
1741            $(
1742                warn!("{}", $item.0);
1743                assert!(attrs.contains(&(
1744                    $item.0.as_ref(), $item.1.as_bytes()
1745                )));
1746            )*
1747
1748        }};
1749    }
1750
1751    #[idm_test]
1752    async fn test_ldap_virtual_attribute_generation(
1753        idms: &IdmServer,
1754        _idms_delayed: &IdmServerDelayed,
1755    ) {
1756        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1757
1758        let ssh_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst";
1759
1760        // Setup a user we want to check.
1761        {
1762            let e1 = entry_init!(
1763                (Attribute::Class, EntryClass::Object.to_value()),
1764                (Attribute::Class, EntryClass::Person.to_value()),
1765                (Attribute::Class, EntryClass::Account.to_value()),
1766                (Attribute::Class, EntryClass::PosixAccount.to_value()),
1767                (Attribute::Name, Value::new_iname("testperson1")),
1768                (
1769                    Attribute::Uuid,
1770                    Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
1771                ),
1772                (Attribute::Description, Value::new_utf8s("testperson1")),
1773                (Attribute::DisplayName, Value::new_utf8s("testperson1")),
1774                (Attribute::GidNumber, Value::new_uint32(12345)),
1775                (Attribute::LoginShell, Value::new_iutf8("/bin/zsh")),
1776                (
1777                    Attribute::SshPublicKey,
1778                    Value::new_sshkey_str("test", ssh_ed25519).expect("Invalid ssh key")
1779                )
1780            );
1781
1782            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
1783            let ce = CreateEvent::new_internal(vec![e1]);
1784            assert!(server_txn
1785                .qs_write
1786                .create(&ce)
1787                .and_then(|_| server_txn.commit())
1788                .is_ok());
1789        }
1790
1791        // Setup the anonymous login.
1792        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
1793        assert_eq!(
1794            anon_t.effective_session,
1795            LdapSession::UnixBind(UUID_ANONYMOUS)
1796        );
1797
1798        // Check that when we request *, we get default list.
1799        let sr = SearchRequest {
1800            msgid: 1,
1801            base: "dc=example,dc=com".to_string(),
1802            scope: LdapSearchScope::Subtree,
1803            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1804            attrs: vec!["*".to_string()],
1805        };
1806        let r1 = ldaps
1807            .do_search(idms, &sr, &anon_t, Source::Internal)
1808            .await
1809            .unwrap();
1810
1811        // The result, and the ldap proto success msg.
1812        assert_eq!(r1.len(), 2);
1813        match &r1[0].op {
1814            LdapOp::SearchResultEntry(lsre) => {
1815                assert_entry_contains!(
1816                    lsre,
1817                    "spn=testperson1@example.com,dc=example,dc=com",
1818                    (Attribute::Class, EntryClass::Object.to_string()),
1819                    (Attribute::Class, EntryClass::Person.to_string()),
1820                    (Attribute::Class, EntryClass::Account.to_string()),
1821                    (Attribute::Class, EntryClass::PosixAccount.to_string()),
1822                    (Attribute::DisplayName, "testperson1"),
1823                    (Attribute::Name, "testperson1"),
1824                    (Attribute::GidNumber, "12345"),
1825                    (Attribute::LoginShell, "/bin/zsh"),
1826                    (Attribute::SshPublicKey, ssh_ed25519),
1827                    (Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
1828                );
1829            }
1830            _ => panic!("Oh no"),
1831        };
1832
1833        // Check that when we request +, we get all attrs and the vattrs
1834        let sr = SearchRequest {
1835            msgid: 1,
1836            base: "dc=example,dc=com".to_string(),
1837            scope: LdapSearchScope::Subtree,
1838            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1839            attrs: vec!["+".to_string()],
1840        };
1841        let r1 = ldaps
1842            .do_search(idms, &sr, &anon_t, Source::Internal)
1843            .await
1844            .unwrap();
1845
1846        // The result, and the ldap proto success msg.
1847        assert_eq!(r1.len(), 2);
1848        match &r1[0].op {
1849            LdapOp::SearchResultEntry(lsre) => {
1850                assert_entry_contains!(
1851                    lsre,
1852                    "spn=testperson1@example.com,dc=example,dc=com",
1853                    (Attribute::ObjectClass, EntryClass::Object.as_ref()),
1854                    (Attribute::ObjectClass, EntryClass::Person.as_ref()),
1855                    (Attribute::ObjectClass, EntryClass::Account.as_ref()),
1856                    (Attribute::ObjectClass, EntryClass::PosixAccount.as_ref()),
1857                    (Attribute::DisplayName, "testperson1"),
1858                    (Attribute::Name, "testperson1"),
1859                    (Attribute::GidNumber, "12345"),
1860                    (Attribute::LoginShell, "/bin/zsh"),
1861                    (Attribute::SshPublicKey, ssh_ed25519),
1862                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
1863                    (
1864                        Attribute::EntryDn,
1865                        "spn=testperson1@example.com,dc=example,dc=com"
1866                    ),
1867                    (Attribute::UidNumber, "12345"),
1868                    (Attribute::Cn, "testperson1"),
1869                    (Attribute::LdapKeys, ssh_ed25519)
1870                );
1871            }
1872            _ => panic!("Oh no"),
1873        };
1874
1875        // Check that when we request an attr by name, we get all of them correctly.
1876        let sr = SearchRequest {
1877            msgid: 1,
1878            base: "dc=example,dc=com".to_string(),
1879            scope: LdapSearchScope::Subtree,
1880            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1881            attrs: vec![
1882                LDAP_ATTR_NAME.to_string(),
1883                Attribute::EntryDn.to_string(),
1884                ATTR_LDAP_KEYS.to_string(),
1885                Attribute::UidNumber.to_string(),
1886            ],
1887        };
1888        let r1 = ldaps
1889            .do_search(idms, &sr, &anon_t, Source::Internal)
1890            .await
1891            .unwrap();
1892
1893        // The result, and the ldap proto success msg.
1894        assert_eq!(r1.len(), 2);
1895        match &r1[0].op {
1896            LdapOp::SearchResultEntry(lsre) => {
1897                assert_entry_contains!(
1898                    lsre,
1899                    "spn=testperson1@example.com,dc=example,dc=com",
1900                    (Attribute::Name, "testperson1"),
1901                    (
1902                        Attribute::EntryDn,
1903                        "spn=testperson1@example.com,dc=example,dc=com"
1904                    ),
1905                    (Attribute::UidNumber, "12345"),
1906                    (Attribute::LdapKeys, ssh_ed25519)
1907                );
1908            }
1909            _ => panic!("Oh no"),
1910        };
1911    }
1912
1913    #[idm_test]
1914    async fn test_ldap_token_privilege_granting(
1915        idms: &IdmServer,
1916        _idms_delayed: &IdmServerDelayed,
1917    ) {
1918        // Setup the ldap server
1919        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
1920
1921        // Prebuild the search req we'll be using this test.
1922        let sr = SearchRequest {
1923            msgid: 1,
1924            base: "dc=example,dc=com".to_string(),
1925            scope: LdapSearchScope::Subtree,
1926            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
1927            attrs: vec![
1928                LDAP_ATTR_NAME,
1929                LDAP_ATTR_MAIL,
1930                LDAP_ATTR_MAIL_PRIMARY,
1931                LDAP_ATTR_MAIL_ALTERNATIVE,
1932                LDAP_ATTR_EMAIL_PRIMARY,
1933                LDAP_ATTR_EMAIL_ALTERNATIVE,
1934            ]
1935            .into_iter()
1936            .map(|s| s.to_string())
1937            .collect(),
1938        };
1939
1940        let sa_uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
1941
1942        // Configure the user account that will have the tokens issued.
1943        // Should be a SERVICE account.
1944        let apitoken = {
1945            // Create a service account,
1946
1947            let e1 = entry_init!(
1948                (Attribute::Class, EntryClass::Object.to_value()),
1949                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
1950                (Attribute::Class, EntryClass::Account.to_value()),
1951                (Attribute::Uuid, Value::Uuid(sa_uuid)),
1952                (Attribute::Name, Value::new_iname("service_permission_test")),
1953                (
1954                    Attribute::DisplayName,
1955                    Value::new_utf8s("service_permission_test")
1956                )
1957            );
1958
1959            // Setup a person with an email
1960            let e2 = entry_init!(
1961                (Attribute::Class, EntryClass::Object.to_value()),
1962                (Attribute::Class, EntryClass::Person.to_value()),
1963                (Attribute::Class, EntryClass::Account.to_value()),
1964                (Attribute::Class, EntryClass::PosixAccount.to_value()),
1965                (Attribute::Name, Value::new_iname("testperson1")),
1966                (
1967                    Attribute::Mail,
1968                    Value::EmailAddress("testperson1@example.com".to_string(), true)
1969                ),
1970                (
1971                    Attribute::Mail,
1972                    Value::EmailAddress("testperson1.alternative@example.com".to_string(), false)
1973                ),
1974                (Attribute::Description, Value::new_utf8s("testperson1")),
1975                (Attribute::DisplayName, Value::new_utf8s("testperson1")),
1976                (Attribute::GidNumber, Value::new_uint32(12345)),
1977                (Attribute::LoginShell, Value::new_iutf8("/bin/zsh"))
1978            );
1979
1980            // Setup an access control for the service account to view mail attrs.
1981
1982            let ct = duration_from_epoch_now();
1983
1984            let mut server_txn = idms.proxy_write(ct).await.unwrap();
1985            let ce = CreateEvent::new_internal(vec![e1, e2]);
1986            assert!(server_txn.qs_write.create(&ce).is_ok());
1987
1988            // idm_people_read_priv
1989            let me = ModifyEvent::new_internal_invalid(
1990                filter!(f_eq(
1991                    Attribute::Name,
1992                    PartialValue::new_iname("idm_people_pii_read")
1993                )),
1994                ModifyList::new_list(vec![Modify::Present(
1995                    Attribute::Member,
1996                    Value::Refer(sa_uuid),
1997                )]),
1998            );
1999            assert!(server_txn.qs_write.modify(&me).is_ok());
2000
2001            // Issue a token
2002            // make it purpose = ldap <- currently purpose isn't supported,
2003            // it's an idea for future.
2004            let gte = GenerateApiTokenEvent::new_internal(sa_uuid, "TestToken", None);
2005
2006            let apitoken = server_txn
2007                .service_account_generate_api_token(&gte, ct)
2008                .expect("Failed to create new apitoken");
2009
2010            assert!(server_txn.commit().is_ok());
2011
2012            apitoken
2013        };
2014
2015        // assert the token fails on non-ldap events token-xchg <- currently
2016        // we don't have purpose so this isn't tested.
2017
2018        // Bind with anonymous, search and show mail attr isn't accessible.
2019        let anon_lbt = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2020        assert_eq!(
2021            anon_lbt.effective_session,
2022            LdapSession::UnixBind(UUID_ANONYMOUS)
2023        );
2024
2025        let r1 = ldaps
2026            .do_search(idms, &sr, &anon_lbt, Source::Internal)
2027            .await
2028            .unwrap();
2029        assert_eq!(r1.len(), 2);
2030        match &r1[0].op {
2031            LdapOp::SearchResultEntry(lsre) => {
2032                assert_entry_contains!(
2033                    lsre,
2034                    "spn=testperson1@example.com,dc=example,dc=com",
2035                    (Attribute::Name, "testperson1")
2036                );
2037            }
2038            _ => panic!("Oh no"),
2039        };
2040
2041        // Inspect the token to get its uuid out.
2042        let jws_verifier = JwsDangerReleaseWithoutVerify::default();
2043
2044        let apitoken_inner = jws_verifier
2045            .verify(&apitoken)
2046            .unwrap()
2047            .from_json::<ApiToken>()
2048            .unwrap();
2049
2050        // Bind using the token as a DN
2051        let sa_lbt = ldaps
2052            .do_bind(idms, "dn=token", &apitoken.to_string())
2053            .await
2054            .unwrap()
2055            .unwrap();
2056        assert_eq!(
2057            sa_lbt.effective_session,
2058            LdapSession::ApiToken(apitoken_inner.clone())
2059        );
2060
2061        // Bind using the token as a pw
2062        let sa_lbt = ldaps
2063            .do_bind(idms, "", &apitoken.to_string())
2064            .await
2065            .unwrap()
2066            .unwrap();
2067        assert_eq!(
2068            sa_lbt.effective_session,
2069            LdapSession::ApiToken(apitoken_inner)
2070        );
2071
2072        // Search and retrieve mail that's now accessible.
2073        let r1 = ldaps
2074            .do_search(idms, &sr, &sa_lbt, Source::Internal)
2075            .await
2076            .unwrap();
2077        assert_eq!(r1.len(), 2);
2078        match &r1[0].op {
2079            LdapOp::SearchResultEntry(lsre) => {
2080                assert_entry_contains!(
2081                    lsre,
2082                    "spn=testperson1@example.com,dc=example,dc=com",
2083                    (Attribute::Name, "testperson1"),
2084                    (Attribute::Mail, "testperson1@example.com"),
2085                    (Attribute::Mail, "testperson1.alternative@example.com"),
2086                    (LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
2087                    (
2088                        LDAP_ATTR_MAIL_ALTERNATIVE,
2089                        "testperson1.alternative@example.com"
2090                    ),
2091                    (LDAP_ATTR_EMAIL_PRIMARY, "testperson1@example.com"),
2092                    (
2093                        LDAP_ATTR_EMAIL_ALTERNATIVE,
2094                        "testperson1.alternative@example.com"
2095                    )
2096                );
2097            }
2098            _ => panic!("Oh no"),
2099        };
2100
2101        // ======= test with a substring search
2102
2103        let sr = SearchRequest {
2104            msgid: 2,
2105            base: "dc=example,dc=com".to_string(),
2106            scope: LdapSearchScope::Subtree,
2107            filter: LdapFilter::And(vec![
2108                LdapFilter::Equality(Attribute::Class.to_string(), "posixAccount".to_string()),
2109                LdapFilter::Substring(
2110                    LDAP_ATTR_MAIL.to_string(),
2111                    LdapSubstringFilter {
2112                        initial: None,
2113                        any: vec![],
2114                        final_: Some("@example.com".to_string()),
2115                    },
2116                ),
2117            ]),
2118            attrs: vec![
2119                LDAP_ATTR_NAME,
2120                LDAP_ATTR_MAIL,
2121                LDAP_ATTR_MAIL_PRIMARY,
2122                LDAP_ATTR_MAIL_ALTERNATIVE,
2123            ]
2124            .into_iter()
2125            .map(|s| s.to_string())
2126            .collect(),
2127        };
2128
2129        let r1 = ldaps
2130            .do_search(idms, &sr, &sa_lbt, Source::Internal)
2131            .await
2132            .unwrap();
2133
2134        assert_eq!(r1.len(), 2);
2135        match &r1[0].op {
2136            LdapOp::SearchResultEntry(lsre) => {
2137                assert_entry_contains!(
2138                    lsre,
2139                    "spn=testperson1@example.com,dc=example,dc=com",
2140                    (Attribute::Name, "testperson1"),
2141                    (Attribute::Mail, "testperson1@example.com"),
2142                    (Attribute::Mail, "testperson1.alternative@example.com"),
2143                    (LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
2144                    (
2145                        LDAP_ATTR_MAIL_ALTERNATIVE,
2146                        "testperson1.alternative@example.com"
2147                    )
2148                );
2149            }
2150            _ => panic!("Oh no"),
2151        };
2152    }
2153
2154    #[idm_test]
2155    async fn test_ldap_virtual_attribute_with_all_attr_search(
2156        idms: &IdmServer,
2157        _idms_delayed: &IdmServerDelayed,
2158    ) {
2159        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2160
2161        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2162
2163        // Setup a user we want to check.
2164        {
2165            let e1 = entry_init!(
2166                (Attribute::Class, EntryClass::Person.to_value()),
2167                (Attribute::Class, EntryClass::Account.to_value()),
2168                (Attribute::Name, Value::new_iname("testperson1")),
2169                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2170                (Attribute::Description, Value::new_utf8s("testperson1")),
2171                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2172            );
2173
2174            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2175            assert!(server_txn
2176                .qs_write
2177                .internal_create(vec![e1])
2178                .and_then(|_| server_txn.commit())
2179                .is_ok());
2180        }
2181
2182        // Setup the anonymous login.
2183        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2184        assert_eq!(
2185            anon_t.effective_session,
2186            LdapSession::UnixBind(UUID_ANONYMOUS)
2187        );
2188
2189        // Check that when we request a virtual attr by name *and* all_attrs we get all the requested values.
2190        let sr = SearchRequest {
2191            msgid: 1,
2192            base: "dc=example,dc=com".to_string(),
2193            scope: LdapSearchScope::Subtree,
2194            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2195            attrs: vec![
2196                "*".to_string(),
2197                // Already being returned
2198                LDAP_ATTR_NAME.to_string(),
2199                // This is a virtual attribute
2200                Attribute::EntryUuid.to_string(),
2201            ],
2202        };
2203        let r1 = ldaps
2204            .do_search(idms, &sr, &anon_t, Source::Internal)
2205            .await
2206            .unwrap();
2207
2208        // The result, and the ldap proto success msg.
2209        assert_eq!(r1.len(), 2);
2210        match &r1[0].op {
2211            LdapOp::SearchResultEntry(lsre) => {
2212                assert_entry_contains!(
2213                    lsre,
2214                    "spn=testperson1@example.com,dc=example,dc=com",
2215                    (Attribute::Name, "testperson1"),
2216                    (Attribute::DisplayName, "testperson1"),
2217                    (Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
2218                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
2219                );
2220            }
2221            _ => panic!("Oh no"),
2222        };
2223    }
2224
2225    // Test behaviour of the 1.1 attribute.
2226    #[idm_test]
2227    async fn test_ldap_one_dot_one_attribute(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2228        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2229
2230        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2231
2232        // Setup a user we want to check.
2233        {
2234            let e1 = entry_init!(
2235                (Attribute::Class, EntryClass::Person.to_value()),
2236                (Attribute::Class, EntryClass::Account.to_value()),
2237                (Attribute::Name, Value::new_iname("testperson1")),
2238                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2239                (Attribute::Description, Value::new_utf8s("testperson1")),
2240                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2241            );
2242
2243            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2244            assert!(server_txn
2245                .qs_write
2246                .internal_create(vec![e1])
2247                .and_then(|_| server_txn.commit())
2248                .is_ok());
2249        }
2250
2251        // Setup the anonymous login.
2252        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2253        assert_eq!(
2254            anon_t.effective_session,
2255            LdapSession::UnixBind(UUID_ANONYMOUS)
2256        );
2257
2258        // If we request only 1.1, we get no attributes.
2259        let sr = SearchRequest {
2260            msgid: 1,
2261            base: "dc=example,dc=com".to_string(),
2262            scope: LdapSearchScope::Subtree,
2263            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2264            attrs: vec!["1.1".to_string()],
2265        };
2266        let r1 = ldaps
2267            .do_search(idms, &sr, &anon_t, Source::Internal)
2268            .await
2269            .unwrap();
2270
2271        // The result, and the ldap proto success msg.
2272        assert_eq!(r1.len(), 2);
2273        match &r1[0].op {
2274            LdapOp::SearchResultEntry(lsre) => {
2275                assert_eq!(
2276                    lsre.dn.as_str(),
2277                    "spn=testperson1@example.com,dc=example,dc=com"
2278                );
2279                assert!(lsre.attributes.is_empty());
2280            }
2281            _ => panic!("Oh no"),
2282        };
2283
2284        // If we request 1.1 and another attr, 1.1 is IGNORED.
2285        let sr = SearchRequest {
2286            msgid: 1,
2287            base: "dc=example,dc=com".to_string(),
2288            scope: LdapSearchScope::Subtree,
2289            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2290            attrs: vec![
2291                "1.1".to_string(),
2292                // This should be present.
2293                Attribute::EntryUuid.to_string(),
2294            ],
2295        };
2296        let r1 = ldaps
2297            .do_search(idms, &sr, &anon_t, Source::Internal)
2298            .await
2299            .unwrap();
2300
2301        // The result, and the ldap proto success msg.
2302        assert_eq!(r1.len(), 2);
2303        match &r1[0].op {
2304            LdapOp::SearchResultEntry(lsre) => {
2305                assert_entry_contains!(
2306                    lsre,
2307                    "spn=testperson1@example.com,dc=example,dc=com",
2308                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
2309                );
2310            }
2311            _ => panic!("Oh no"),
2312        };
2313    }
2314
2315    #[idm_test]
2316    async fn test_ldap_rootdse_basedn_change(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2317        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2318
2319        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2320        assert_eq!(
2321            anon_t.effective_session,
2322            LdapSession::UnixBind(UUID_ANONYMOUS)
2323        );
2324
2325        let sr = SearchRequest {
2326            msgid: 1,
2327            base: "".to_string(),
2328            scope: LdapSearchScope::Base,
2329            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
2330            attrs: vec!["*".to_string()],
2331        };
2332        let r1 = ldaps
2333            .do_search(idms, &sr, &anon_t, Source::Internal)
2334            .await
2335            .unwrap();
2336
2337        trace!(?r1);
2338
2339        // The result, and the ldap proto success msg.
2340        assert_eq!(r1.len(), 2);
2341        match &r1[0].op {
2342            LdapOp::SearchResultEntry(lsre) => {
2343                assert_entry_contains!(
2344                    lsre,
2345                    "",
2346                    (Attribute::ObjectClass, "top"),
2347                    ("vendorname", "Kanidm Project"),
2348                    ("supportedldapversion", "3"),
2349                    ("defaultnamingcontext", "dc=example,dc=com")
2350                );
2351            }
2352            _ => panic!("Oh no"),
2353        };
2354
2355        drop(ldaps);
2356
2357        // Change the domain basedn
2358
2359        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2360        // make the admin a valid posix account
2361        let me_posix = ModifyEvent::new_internal_invalid(
2362            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
2363            ModifyList::new_purge_and_set(
2364                Attribute::DomainLdapBasedn,
2365                Value::new_iutf8("o=kanidmproject"),
2366            ),
2367        );
2368        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());
2369
2370        assert!(idms_prox_write.commit().is_ok());
2371
2372        // Now re-test
2373        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2374
2375        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2376        assert_eq!(
2377            anon_t.effective_session,
2378            LdapSession::UnixBind(UUID_ANONYMOUS)
2379        );
2380
2381        let sr = SearchRequest {
2382            msgid: 1,
2383            base: "".to_string(),
2384            scope: LdapSearchScope::Base,
2385            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
2386            attrs: vec!["*".to_string()],
2387        };
2388        let r1 = ldaps
2389            .do_search(idms, &sr, &anon_t, Source::Internal)
2390            .await
2391            .unwrap();
2392
2393        trace!(?r1);
2394
2395        // The result, and the ldap proto success msg.
2396        assert_eq!(r1.len(), 2);
2397        match &r1[0].op {
2398            LdapOp::SearchResultEntry(lsre) => {
2399                assert_entry_contains!(
2400                    lsre,
2401                    "",
2402                    (Attribute::ObjectClass, "top"),
2403                    ("vendorname", "Kanidm Project"),
2404                    ("supportedldapversion", "3"),
2405                    ("defaultnamingcontext", "o=kanidmproject")
2406                );
2407            }
2408            _ => panic!("Oh no"),
2409        };
2410    }
2411
2412    #[idm_test]
2413    async fn test_ldap_sssd_compat(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2414        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2415
2416        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2417
2418        // Setup a user we want to check.
2419        {
2420            let e1 = entry_init!(
2421                (Attribute::Class, EntryClass::Person.to_value()),
2422                (Attribute::Class, EntryClass::Account.to_value()),
2423                (Attribute::Class, EntryClass::PosixAccount.to_value()),
2424                (Attribute::Name, Value::new_iname("testperson1")),
2425                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2426                (Attribute::GidNumber, Value::Uint32(12345)),
2427                (Attribute::Description, Value::new_utf8s("testperson1")),
2428                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2429            );
2430
2431            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2432            assert!(server_txn
2433                .qs_write
2434                .internal_create(vec![e1])
2435                .and_then(|_| server_txn.commit())
2436                .is_ok());
2437        }
2438
2439        // Setup the anonymous login.
2440        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2441        assert_eq!(
2442            anon_t.effective_session,
2443            LdapSession::UnixBind(UUID_ANONYMOUS)
2444        );
2445
2446        // SSSD tries to just search for silly attrs all the time. We ignore them.
2447        let sr = SearchRequest {
2448            msgid: 1,
2449            base: "dc=example,dc=com".to_string(),
2450            scope: LdapSearchScope::Subtree,
2451            filter: LdapFilter::And(vec![
2452                LdapFilter::Equality(Attribute::Class.to_string(), "sudohost".to_string()),
2453                LdapFilter::Substring(
2454                    Attribute::SudoHost.to_string(),
2455                    LdapSubstringFilter {
2456                        initial: Some("a".to_string()),
2457                        any: vec!["x".to_string()],
2458                        final_: Some("z".to_string()),
2459                    },
2460                ),
2461            ]),
2462            attrs: vec![
2463                "*".to_string(),
2464                // Already being returned
2465                LDAP_ATTR_NAME.to_string(),
2466                // This is a virtual attribute
2467                Attribute::EntryUuid.to_string(),
2468            ],
2469        };
2470        let r1 = ldaps
2471            .do_search(idms, &sr, &anon_t, Source::Internal)
2472            .await
2473            .unwrap();
2474
2475        // Empty results and ldap proto success msg.
2476        assert_eq!(r1.len(), 1);
2477
2478        // Second search
2479
2480        let sr = SearchRequest {
2481            msgid: 1,
2482            base: "dc=example,dc=com".to_string(),
2483            scope: LdapSearchScope::Subtree,
2484            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
2485            attrs: vec![
2486                "uid".to_string(),
2487                "uidNumber".to_string(),
2488                "gidNumber".to_string(),
2489                "gecos".to_string(),
2490                "cn".to_string(),
2491                "entryuuid".to_string(),
2492            ],
2493        };
2494        let r1 = ldaps
2495            .do_search(idms, &sr, &anon_t, Source::Internal)
2496            .await
2497            .unwrap();
2498
2499        trace!(?r1);
2500
2501        // The result, and the ldap proto success msg.
2502        assert_eq!(r1.len(), 2);
2503        match &r1[0].op {
2504            LdapOp::SearchResultEntry(lsre) => {
2505                assert_entry_contains!(
2506                    lsre,
2507                    "spn=testperson1@example.com,dc=example,dc=com",
2508                    (Attribute::Uid, "testperson1"),
2509                    (Attribute::Cn, "testperson1"),
2510                    (Attribute::Gecos, "testperson1"),
2511                    (Attribute::UidNumber, "12345"),
2512                    (Attribute::GidNumber, "12345"),
2513                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
2514                );
2515            }
2516            _ => panic!("Oh no"),
2517        };
2518    }
2519
2520    #[idm_test]
2521    async fn test_ldap_compare_request(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
2522        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2523
2524        // Setup a user we want to check.
2525        {
2526            let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
2527
2528            let e1 = entry_init!(
2529                (Attribute::Class, EntryClass::Person.to_value()),
2530                (Attribute::Class, EntryClass::Account.to_value()),
2531                (Attribute::Class, EntryClass::PosixAccount.to_value()),
2532                (Attribute::Name, Value::new_iname("testperson1")),
2533                (Attribute::Uuid, Value::Uuid(acct_uuid)),
2534                (Attribute::GidNumber, Value::Uint32(12345)),
2535                (Attribute::Description, Value::new_utf8s("testperson1")),
2536                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2537            );
2538
2539            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2540            assert!(server_txn
2541                .qs_write
2542                .internal_create(vec![e1])
2543                .and_then(|_| server_txn.commit())
2544                .is_ok());
2545        }
2546
2547        // Setup the anonymous login.
2548        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2549        assert_eq!(
2550            anon_t.effective_session,
2551            LdapSession::UnixBind(UUID_ANONYMOUS)
2552        );
2553
2554        #[track_caller]
2555        fn assert_compare_result(r: &[LdapMsg], code: &LdapResultCode) {
2556            assert_eq!(r.len(), 1);
2557            match &r[0].op {
2558                LdapOp::CompareResult(lcr) => {
2559                    assert_eq!(&lcr.code, code);
2560                }
2561                _ => panic!("Oh no"),
2562            };
2563        }
2564
2565        let cr = CompareRequest {
2566            msgid: 1,
2567            entry: "name=testperson1,dc=example,dc=com".to_string(),
2568            atype: Attribute::Name.to_string(),
2569            val: "testperson1".to_string(),
2570        };
2571
2572        assert_compare_result(
2573            &ldaps
2574                .do_compare(idms, &cr, &anon_t, Source::Internal)
2575                .await
2576                .unwrap(),
2577            &LdapResultCode::CompareTrue,
2578        );
2579
2580        let cr = CompareRequest {
2581            msgid: 1,
2582            entry: "name=testperson1,dc=example,dc=com".to_string(),
2583            atype: Attribute::GidNumber.to_string(),
2584            val: "12345".to_string(),
2585        };
2586
2587        assert_compare_result(
2588            &ldaps
2589                .do_compare(idms, &cr, &anon_t, Source::Internal)
2590                .await
2591                .unwrap(),
2592            &LdapResultCode::CompareTrue,
2593        );
2594
2595        let cr = CompareRequest {
2596            msgid: 1,
2597            entry: "name=testperson1,dc=example,dc=com".to_string(),
2598            atype: Attribute::Name.to_string(),
2599            val: "other".to_string(),
2600        };
2601        assert_compare_result(
2602            &ldaps
2603                .do_compare(idms, &cr, &anon_t, Source::Internal)
2604                .await
2605                .unwrap(),
2606            &LdapResultCode::CompareFalse,
2607        );
2608
2609        let cr = CompareRequest {
2610            msgid: 1,
2611            entry: "name=other,dc=example,dc=com".to_string(),
2612            atype: Attribute::Name.to_string(),
2613            val: "other".to_string(),
2614        };
2615        assert_compare_result(
2616            &ldaps
2617                .do_compare(idms, &cr, &anon_t, Source::Internal)
2618                .await
2619                .unwrap(),
2620            &LdapResultCode::NoSuchObject,
2621        );
2622
2623        let cr = CompareRequest {
2624            msgid: 1,
2625            entry: "invalidentry".to_string(),
2626            atype: Attribute::Name.to_string(),
2627            val: "other".to_string(),
2628        };
2629        assert!(&ldaps
2630            .do_compare(idms, &cr, &anon_t, Source::Internal)
2631            .await
2632            .is_err());
2633
2634        let cr = CompareRequest {
2635            msgid: 1,
2636            entry: "name=other,dc=example,dc=com".to_string(),
2637            atype: "invalid".to_string(),
2638            val: "other".to_string(),
2639        };
2640        assert_eq!(
2641            &ldaps
2642                .do_compare(idms, &cr, &anon_t, Source::Internal)
2643                .await
2644                .unwrap_err(),
2645            &OperationError::InvalidAttributeName("invalid".to_string()),
2646        );
2647    }
2648
2649    #[idm_test]
2650    async fn test_ldap_maximum_queryable_attributes(
2651        idms: &IdmServer,
2652        _idms_delayed: &IdmServerDelayed,
2653    ) {
2654        // Set the max queryable attrs to 2
2655
2656        let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
2657
2658        let set_ldap_maximum_queryable_attrs = ModifyEvent::new_internal_invalid(
2659            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
2660            ModifyList::new_purge_and_set(Attribute::LdapMaxQueryableAttrs, Value::Uint32(2)),
2661        );
2662        assert!(server_txn
2663            .qs_write
2664            .modify(&set_ldap_maximum_queryable_attrs)
2665            .and_then(|_| server_txn.commit())
2666            .is_ok());
2667
2668        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
2669
2670        let usr_uuid = Uuid::new_v4();
2671        let grp_uuid = Uuid::new_v4();
2672        let app_uuid = Uuid::new_v4();
2673        let app_name = "testapp1";
2674
2675        // Setup person, group and application
2676        {
2677            let e1 = entry_init!(
2678                (Attribute::Class, EntryClass::Object.to_value()),
2679                (Attribute::Class, EntryClass::Account.to_value()),
2680                (Attribute::Class, EntryClass::Person.to_value()),
2681                (Attribute::Name, Value::new_iname("testperson1")),
2682                (Attribute::Uuid, Value::Uuid(usr_uuid)),
2683                (Attribute::Description, Value::new_utf8s("testperson1")),
2684                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
2685            );
2686
2687            let e2 = entry_init!(
2688                (Attribute::Class, EntryClass::Object.to_value()),
2689                (Attribute::Class, EntryClass::Group.to_value()),
2690                (Attribute::Name, Value::new_iname("testgroup1")),
2691                (Attribute::Uuid, Value::Uuid(grp_uuid))
2692            );
2693
2694            let e3 = entry_init!(
2695                (Attribute::Class, EntryClass::Object.to_value()),
2696                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
2697                (Attribute::Class, EntryClass::Application.to_value()),
2698                (Attribute::Name, Value::new_iname(app_name)),
2699                (Attribute::Uuid, Value::Uuid(app_uuid)),
2700                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
2701            );
2702
2703            let ct = duration_from_epoch_now();
2704            let mut server_txn = idms.proxy_write(ct).await.unwrap();
2705            assert!(server_txn
2706                .qs_write
2707                .internal_create(vec![e1, e2, e3])
2708                .and_then(|_| server_txn.commit())
2709                .is_ok());
2710        }
2711
2712        // Setup the anonymous login
2713        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
2714        assert_eq!(
2715            anon_t.effective_session,
2716            LdapSession::UnixBind(UUID_ANONYMOUS)
2717        );
2718
2719        let invalid_search = SearchRequest {
2720            msgid: 1,
2721            base: "dc=example,dc=com".to_string(),
2722            scope: LdapSearchScope::Subtree,
2723            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
2724            attrs: vec![
2725                "objectClass".to_string(),
2726                "cn".to_string(),
2727                "givenName".to_string(),
2728            ],
2729        };
2730
2731        let valid_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!["objectClass: person".to_string()],
2737        };
2738
2739        let invalid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
2740            .do_search(idms, &invalid_search, &anon_t, Source::Internal)
2741            .await;
2742
2743        let valid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
2744            .do_search(idms, &valid_search, &anon_t, Source::Internal)
2745            .await;
2746
2747        assert_eq!(invalid_res, Err(OperationError::ResourceLimit));
2748        assert!(valid_res.is_ok());
2749    }
2750}