Repository: lldap/lldap (language: rust)

Issue:
[FEATURE REQUEST]: wildcards should expand to custom attributes



Discussion / hints:

Fix patch:
diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs
index a3584ea7..21f47c54 100644
--- a/server/src/domain/ldap/group.rs
+++ b/server/src/domain/ldap/group.rs
@@ -7,31 +7,28 @@ use tracing::{debug, instrument, warn};
 use crate::domain::{
     deserialize::deserialize_attribute_value,
     handler::{GroupListerBackendHandler, GroupRequestFilter},
-    ldap::error::LdapError,
+    ldap::{
+        error::{LdapError, LdapResult},
+        utils::{
+            expand_attribute_wildcards, get_custom_attribute,
+            get_group_id_from_distinguished_name_or_plain_name,
+            get_user_id_from_distinguished_name_or_plain_name, map_group_field, ExpandedAttributes,
+            GroupFieldType, LdapInfo,
+        },
+    },
     schema::{PublicSchema, SchemaGroupAttributeExtractor},
     types::{AttributeName, AttributeType, Group, LdapObjectClass, UserId, Uuid},
 };
 
-use super::{
-    error::LdapResult,
-    utils::{
-        expand_attribute_wildcards, get_custom_attribute,
-        get_group_id_from_distinguished_name_or_plain_name,
-        get_user_id_from_distinguished_name_or_plain_name, map_group_field, GroupFieldType,
-        LdapInfo,
-    },
-};
-
 pub fn get_group_attribute(
     group: &Group,
     base_dn_str: &str,
-    attribute: &str,
+    attribute: &AttributeName,
     user_filter: &Option<UserId>,
     ignored_group_attributes: &[AttributeName],
     schema: &PublicSchema,
 ) -> Option<Vec<Vec<u8>>> {
-    let attribute = AttributeName::from(attribute);
-    let attribute_values = match map_group_field(&attribute, schema) {
+    let attribute_values = match map_group_field(attribute, schema) {
         GroupFieldType::ObjectClass => {
             let mut classes = vec![b"groupOfUniqueNames".to_vec()];
             classes.extend(
@@ -74,12 +71,12 @@ pub fn get_group_attribute(
                 )
             }
             _ => {
-                if ignored_group_attributes.contains(&attribute) {
+                if ignored_group_attributes.contains(attribute) {
                     return None;
                 }
                 get_custom_attribute::<SchemaGroupAttributeExtractor>(
                         &group.attributes,
-                        &attribute,
+                        attribute,
                         schema,
                     ).or_else(||{warn!(
                             r#"Ignoring unrecognized group attribute: {}\n\
@@ -105,33 +102,42 @@ const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] = &[
     "entryuuid",
 ];
 
-fn expand_group_attribute_wildcards(attributes: &[String]) -> Vec<&str> {
+fn expand_group_attribute_wildcards(attributes: &[String]) -> ExpandedAttributes {
     expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS)
 }
 
 fn make_ldap_search_group_result_entry(
     group: Group,
     base_dn_str: &str,
-    expanded_attributes: &[&str],
+    mut expanded_attributes: ExpandedAttributes,
     user_filter: &Option<UserId>,
     ignored_group_attributes: &[AttributeName],
     schema: &PublicSchema,
 ) -> LdapSearchResultEntry {
+    if expanded_attributes.include_custom_attributes {
+        expanded_attributes.attribute_keys.extend(
+            group
+                .attributes
+                .iter()
+                .map(|a| (a.name.clone(), a.name.to_string())),
+        );
+    }
     LdapSearchResultEntry {
         dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str),
         attributes: expanded_attributes
-            .iter()
-            .filter_map(|a| {
+            .attribute_keys
+            .into_iter()
+            .filter_map(|(attribute, name)| {
                 let values = get_group_attribute(
                     &group,
                     base_dn_str,
-                    a,
+                    &attribute,
                     user_filter,
                     ignored_group_attributes,
                     schema,
                 )?;
                 Some(LdapPartialAttribute {
-                    atype: a.to_string(),
+                    atype: name,
                     vals: values,
                 })
             })
@@ -295,7 +301,7 @@ pub fn convert_groups_to_ldap_op<'a>(
         LdapOp::SearchResultEntry(make_ldap_search_group_result_entry(
             g,
             &ldap_info.base_dn_str,
-            expanded_attributes.as_ref().unwrap(),
+            expanded_attributes.clone().unwrap(),
             user_filter,
             &ldap_info.ignored_group_attributes,
             schema,
diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs
index 325200b9..9f1d9857 100644
--- a/server/src/domain/ldap/user.rs
+++ b/server/src/domain/ldap/user.rs
@@ -12,8 +12,8 @@ use crate::domain::{
         utils::{
             expand_attribute_wildcards, get_custom_attribute,
             get_group_id_from_distinguished_name_or_plain_name,
-            get_user_id_from_distinguished_name_or_plain_name, map_user_field, LdapInfo,
-            UserFieldType,
+            get_user_id_from_distinguished_name_or_plain_name, map_user_field, ExpandedAttributes,
+            LdapInfo, UserFieldType,
         },
     },
     schema::{PublicSchema, SchemaUserAttributeExtractor},
@@ -25,14 +25,13 @@ use crate::domain::{
 
 pub fn get_user_attribute(
     user: &User,
-    attribute: &str,
+    attribute: &AttributeName,
     base_dn_str: &str,
     groups: Option<&[GroupDetails]>,
     ignored_user_attributes: &[AttributeName],
     schema: &PublicSchema,
 ) -> Option<Vec<Vec<u8>>> {
-    let attribute = AttributeName::from(attribute);
-    let attribute_values = match map_user_field(&attribute, schema) {
+    let attribute_values = match map_user_field(attribute, schema) {
         UserFieldType::ObjectClass => {
             let mut classes = vec![
                 b"inetOrgPerson".to_vec(),
@@ -93,12 +92,12 @@ pub fn get_user_attribute(
                 )
             }
             _ => {
-                if ignored_user_attributes.contains(&attribute) {
+                if ignored_user_attributes.contains(attribute) {
                     return None;
                 }
                 get_custom_attribute::<SchemaUserAttributeExtractor>(
                     &user.attributes,
-                    &attribute,
+                    attribute,
                     schema,
                 )
                 .or_else(|| {
@@ -134,27 +133,34 @@ const ALL_USER_ATTRIBUTE_KEYS: &[&str] = &[
 fn make_ldap_search_user_result_entry(
     user: User,
     base_dn_str: &str,
-    expanded_attributes: &[&str],
+    mut expanded_attributes: ExpandedAttributes,
     groups: Option<&[GroupDetails]>,
     ignored_user_attributes: &[AttributeName],
     schema: &PublicSchema,
 ) -> LdapSearchResultEntry {
-    let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str);
+    if expanded_attributes.include_custom_attributes {
+        expanded_attributes.attribute_keys.extend(
+            user.attributes
+                .iter()
+                .map(|a| (a.name.clone(), a.name.to_string())),
+        );
+    }
     LdapSearchResultEntry {
-        dn,
+        dn: format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str),
         attributes: expanded_attributes
-            .iter()
-            .filter_map(|a| {
+            .attribute_keys
+            .into_iter()
+            .filter_map(|(attribute, name)| {
                 let values = get_user_attribute(
                     &user,
-                    a,
+                    &attribute,
                     base_dn_str,
                     groups,
                     ignored_user_attributes,
                     schema,
                 )?;
                 Some(LdapPartialAttribute {
-                    atype: a.to_string(),
+                    atype: name,
                     vals: values,
                 })
             })
@@ -295,7 +301,7 @@ fn convert_user_filter(
     }
 }
 
-fn expand_user_attribute_wildcards(attributes: &[String]) -> Vec<&str> {
+fn expand_user_attribute_wildcards(attributes: &[String]) -> ExpandedAttributes {
     expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS)
 }
 
@@ -334,7 +340,7 @@ pub fn convert_users_to_ldap_op<'a>(
         LdapOp::SearchResultEntry(make_ldap_search_user_result_entry(
             u.user,
             &ldap_info.base_dn_str,
-            expanded_attributes.as_ref().unwrap(),
+            expanded_attributes.clone().unwrap(),
             u.groups.as_deref(),
             &ldap_info.ignored_user_attributes,
             schema,
diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs
index 835213c0..4ce055f0 100644
--- a/server/src/domain/ldap/utils.rs
+++ b/server/src/domain/ldap/utils.rs
@@ -1,5 +1,6 @@
+use std::collections::BTreeMap;
+
 use chrono::{NaiveDateTime, TimeZone};
-use itertools::Itertools;
 use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode};
 use tracing::{debug, instrument, warn};
 
@@ -137,30 +138,39 @@ pub fn get_group_id_from_distinguished_name_or_plain_name(
     }
 }
 
+#[derive(Clone)]
+pub struct ExpandedAttributes {
+    // Lowercase name to original name.
+    pub attribute_keys: BTreeMap<AttributeName, String>,
+    pub include_custom_attributes: bool,
+}
+
 #[instrument(skip(all_attribute_keys), level = "debug")]
-pub fn expand_attribute_wildcards<'a>(
-    ldap_attributes: &'a [String],
-    all_attribute_keys: &'a [&'static str],
-) -> Vec<&'a str> {
-    let extra_attributes =
+pub fn expand_attribute_wildcards(
+    ldap_attributes: &[String],
+    all_attribute_keys: &[&'static str],
+) -> ExpandedAttributes {
+    let mut include_custom_attributes = false;
+    let mut attributes_out: BTreeMap<_, _> = ldap_attributes
+        .iter()
+        .filter(|&s| s != "*" && s != "+" && s != "1.1")
+        .map(|s| (AttributeName::from(s), s.to_string()))
+        .collect();
+    attributes_out.extend(
         if ldap_attributes.iter().any(|x| x == "*") || ldap_attributes.is_empty() {
+            include_custom_attributes = true;
             all_attribute_keys
         } else {
             &[]
         }
         .iter()
-        .copied();
-    let attributes_out = ldap_attributes
-        .iter()
-        .map(|s| s.as_str())
-        .filter(|&s| s != "*" && s != "+" && s != "1.1");
-
-    // Deduplicate, preserving order
-    let resolved_attributes = itertools::chain(attributes_out, extra_attributes)
-        .unique_by(|a| a.to_ascii_lowercase())
-        .collect_vec();
-    debug!(?resolved_attributes);
-    resolved_attributes
+        .map(|&s| (AttributeName::from(s), s.to_string())),
+    );
+    debug!(?attributes_out);
+    ExpandedAttributes {
+        attribute_keys: attributes_out,
+        include_custom_attributes,
+    }
 }
 
 pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> bool {
diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs
index f9f2ea17..a3817378 100644
--- a/server/src/infra/ldap_handler.rs
+++ b/server/src/infra/ldap_handler.rs
@@ -775,13 +775,13 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
                 message: "Too many search results".to_string(),
             });
         }
-
+        let requested_attribute = AttributeName::from(&request.atype);
         match entries.first() {
             Some(LdapOp::SearchResultEntry(entry)) => {
-                let available = entry
-                    .attributes
-                    .iter()
-                    .any(|attr| attr.atype == request.atype && attr.vals.contains(&request.val));
+                let available = entry.attributes.iter().any(|attr| {
+                    AttributeName::from(&attr.atype) == requested_attribute
+                        && attr.vals.contains(&request.val)
+                });
                 Ok(vec![LdapOp::CompareResult(LdapResultOp {
                     code: if available {
                         LdapResultCode::CompareTrue
@@ -1288,42 +1288,42 @@ mod tests {
                     dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
                     attributes: vec![
                         LdapPartialAttribute {
-                            atype: "objectClass".to_string(),
-                            vals: vec![
-                                b"inetOrgPerson".to_vec(),
-                                b"posixAccount".to_vec(),
-                                b"mailAccount".to_vec(),
-                                b"person".to_vec(),
-                                b"customUserClass".to_vec(),
-                            ]
+                            atype: "cn".to_string(),
+                            vals: vec!["Bôb Böbberson".to_string().into_bytes()]
                         },
                         LdapPartialAttribute {
-                            atype: "uid".to_string(),
-                            vals: vec![b"bob_1".to_vec()]
+                            atype: "createTimestamp".to_string(),
+                            vals: vec![b"1970-01-01T00:00:00+00:00".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "mail".to_string(),
-                            vals: vec![b"bob@bobmail.bob".to_vec()]
+                            atype: "entryUuid".to_string(),
+                            vals: vec![b"698e1d5f-7a40-3151-8745-b9b8a37839da".to_vec()]
                         },
                         LdapPartialAttribute {
                             atype: "givenName".to_string(),
                             vals: vec!["Bôb".to_string().into_bytes()]
                         },
                         LdapPartialAttribute {
-                            atype: "sn".to_string(),
-                            vals: vec!["Böbberson".to_string().into_bytes()]
+                            atype: "mail".to_string(),
+                            vals: vec![b"bob@bobmail.bob".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "cn".to_string(),
-                            vals: vec!["Bôb Böbberson".to_string().into_bytes()]
+                            atype: "objectClass".to_string(),
+                            vals: vec![
+                                b"inetOrgPerson".to_vec(),
+                                b"posixAccount".to_vec(),
+                                b"mailAccount".to_vec(),
+                                b"person".to_vec(),
+                                b"customUserClass".to_vec(),
+                            ]
                         },
                         LdapPartialAttribute {
-                            atype: "createTimestamp".to_string(),
-                            vals: vec![b"1970-01-01T00:00:00+00:00".to_vec()]
+                            atype: "sn".to_string(),
+                            vals: vec!["Böbberson".to_string().into_bytes()]
                         },
                         LdapPartialAttribute {
-                            atype: "entryUuid".to_string(),
-                            vals: vec![b"698e1d5f-7a40-3151-8745-b9b8a37839da".to_vec()]
+                            atype: "uid".to_string(),
+                            vals: vec![b"bob_1".to_vec()]
                         },
                     ],
                 }),
@@ -1331,46 +1331,46 @@ mod tests {
                     dn: "uid=jim,ou=people,dc=example,dc=com".to_string(),
                     attributes: vec![
                         LdapPartialAttribute {
-                            atype: "objectClass".to_string(),
-                            vals: vec![
-                                b"inetOrgPerson".to_vec(),
-                                b"posixAccount".to_vec(),
-                                b"mailAccount".to_vec(),
-                                b"person".to_vec(),
-                                b"customUserClass".to_vec(),
-                            ]
+                            atype: "cn".to_string(),
+                            vals: vec![b"Jimminy Cricket".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "uid".to_string(),
-                            vals: vec![b"jim".to_vec()]
+                            atype: "createTimestamp".to_string(),
+                            vals: vec![b"2014-07-08T09:10:11+00:00".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "mail".to_string(),
-                            vals: vec![b"jim@cricket.jim".to_vec()]
+                            atype: "entryUuid".to_string(),
+                            vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()]
                         },
                         LdapPartialAttribute {
                             atype: "givenName".to_string(),
                             vals: vec![b"Jim".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "sn".to_string(),
-                            vals: vec![b"Cricket".to_vec()]
+                            atype: "jpegPhoto".to_string(),
+                            vals: vec![JpegPhoto::for_tests().into_bytes()]
                         },
                         LdapPartialAttribute {
-                            atype: "cn".to_string(),
-                            vals: vec![b"Jimminy Cricket".to_vec()]
+                            atype: "mail".to_string(),
+                            vals: vec![b"jim@cricket.jim".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "createTimestamp".to_string(),
-                            vals: vec![b"2014-07-08T09:10:11+00:00".to_vec()]
+                            atype: "objectClass".to_string(),
+                            vals: vec![
+                                b"inetOrgPerson".to_vec(),
+                                b"posixAccount".to_vec(),
+                                b"mailAccount".to_vec(),
+                                b"person".to_vec(),
+                                b"customUserClass".to_vec(),
+                            ]
                         },
                         LdapPartialAttribute {
-                            atype: "entryUuid".to_string(),
-                            vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()]
+                            atype: "sn".to_string(),
+                            vals: vec![b"Cricket".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "jpegPhoto".to_string(),
-                            vals: vec![JpegPhoto::for_tests().into_bytes()]
+                            atype: "uid".to_string(),
+                            vals: vec![b"jim".to_vec()]
                         },
                     ],
                 }),
@@ -1423,53 +1423,53 @@ mod tests {
                 LdapOp::SearchResultEntry(LdapSearchResultEntry {
                     dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
                     attributes: vec![
-                        LdapPartialAttribute {
-                            atype: "objectClass".to_string(),
-                            vals: vec![b"groupOfUniqueNames".to_vec(),]
-                        },
                         LdapPartialAttribute {
                             atype: "cn".to_string(),
                             vals: vec![b"group_1".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "uniqueMember".to_string(),
-                            vals: vec![
-                                b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
-                                b"uid=john,ou=people,dc=example,dc=com".to_vec(),
-                            ]
+                            atype: "entryDN".to_string(),
+                            vals: vec![b"uid=group_1,ou=groups,dc=example,dc=com".to_vec()],
                         },
                         LdapPartialAttribute {
                             atype: "entryUuid".to_string(),
                             vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
                         },
                         LdapPartialAttribute {
-                            atype: "entryDN".to_string(),
-                            vals: vec![b"uid=group_1,ou=groups,dc=example,dc=com".to_vec()],
+                            atype: "objectClass".to_string(),
+                            vals: vec![b"groupOfUniqueNames".to_vec(),]
+                        },
+                        LdapPartialAttribute {
+                            atype: "uniqueMember".to_string(),
+                            vals: vec![
+                                b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
+                                b"uid=john,ou=people,dc=example,dc=com".to_vec(),
+                            ]
                         },
                     ],
                 }),
                 LdapOp::SearchResultEntry(LdapSearchResultEntry {
                     dn: "cn=BestGroup,ou=groups,dc=example,dc=com".to_string(),
                     attributes: vec![
-                        LdapPartialAttribute {
-                            atype: "objectClass".to_string(),
-                            vals: vec![b"groupOfUniqueNames".to_vec(),]
-                        },
                         LdapPartialAttribute {
                             atype: "cn".to_string(),
                             vals: vec![b"BestGroup".to_vec()]
                         },
                         LdapPartialAttribute {
-                            atype: "uniqueMember".to_string(),
-                            vals: vec![b"uid=john,ou=people,dc=example,dc=com".to_vec()]
+                            atype: "entryDN".to_string(),
+                            vals: vec![b"uid=BestGroup,ou=groups,dc=example,dc=com".to_vec()],
                         },
                         LdapPartialAttribute {
                             atype: "entryUuid".to_string(),
                             vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
                         },
                         LdapPartialAttribute {
-                            atype: "entryDN".to_string(),
-                            vals: vec![b"uid=BestGroup,ou=groups,dc=example,dc=com".to_vec()],
+                            atype: "objectClass".to_string(),
+                            vals: vec![b"groupOfUniqueNames".to_vec(),]
+                        },
+                        LdapPartialAttribute {
+                            atype: "uniqueMember".to_string(),
+                            vals: vec![b"uid=john,ou=people,dc=example,dc=com".to_vec()]
                         },
                     ],
                 }),
@@ -2028,6 +2028,10 @@ mod tests {
                 LdapOp::SearchResultEntry(LdapSearchResultEntry {
                     dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
                     attributes: vec![
+                        LdapPartialAttribute {
+                            atype: "cn".to_string(),
+                            vals: vec!["Bôb Böbberson".to_string().into_bytes()]
+                        },
                         LdapPartialAttribute {
                             atype: "objectClass".to_string(),
                             vals: vec![
@@ -2036,25 +2040,21 @@ mod tests {
                                 b"mailAccount".to_vec(),
                                 b"person".to_vec(),
                                 b"customUserClass".to_vec(),
-                            ]
-                        },
-                        LdapPartialAttribute {
-                            atype: "cn".to_string(),
-                            vals: vec!["Bôb Böbberson".to_string().into_bytes()]
+                            ],
                         },
                     ],
                 }),
                 LdapOp::SearchResultEntry(LdapSearchResultEntry {
                     dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
                     attributes: vec![
-                        LdapPartialAttribute {
-                            atype: "objectClass".to_string(),
-                            vals: vec![b"groupOfUniqueNames".to_vec(),]
-                        },
                         LdapPartialAttribute {
                             atype: "cn".to_string(),
                             vals: vec![b"group_1".to_vec()]
                         },
+                        LdapPartialAttribute {
+                            atype: "objectClass".to_string(),
+                            vals: vec![b"groupOfUniqueNames".to_vec(),]
+                        },
                     ],
                 }),
                 make_search_success(),
@@ -2114,35 +2114,13 @@ mod tests {
                 dn: "uid=bob_1,ou=people,dc=example,dc=com".to_string(),
                 attributes: vec![
                     LdapPartialAttribute {
-                        atype: "objectclass".to_string(),
-                        vals: vec![
-                            b"inetOrgPerson".to_vec(),
-                            b"posixAccount".to_vec(),
-                            b"mailAccount".to_vec(),
-                            b"person".to_vec(),
-                            b"customUserClass".to_vec(),
-                        ],
-                    },
-                    LdapPartialAttribute {
-                        atype: "uid".to_string(),
-                        vals: vec![b"bob_1".to_vec()],
-                    },
-                    LdapPartialAttribute {
-                        atype: "mail".to_string(),
-                        vals: vec![b"bob@bobmail.bob".to_vec()],
-                    },
-                    LdapPartialAttribute {
-                        atype: "sn".to_string(),
-                        vals: vec!["Böbberson".to_string().into_bytes()],
+                        atype: "avatar".to_string(),
+                        vals: vec![JpegPhoto::for_tests().into_bytes()],
                     },
                     LdapPartialAttribute {
                         atype: "cn".to_string(),
                         vals: vec!["Bôb Böbberson".to_string().into_bytes()],
                     },
-                    LdapPartialAttribute {
-                        atype: "jpegPhoto".to_string(),
-                        vals: vec![JpegPhoto::for_tests().into_bytes()],
-                    },
                     LdapPartialAttribute {
                         atype: "createtimestamp".to_string(),
                         vals: vec![chrono::Utc
@@ -2155,6 +2133,36 @@ mod tests {
                         atype: "entryuuid".to_string(),
                         vals: vec![b"b4ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
                     },
+                    LdapPartialAttribute {
+                        atype: "jpegPhoto".to_string(),
+                        vals: vec![JpegPhoto::for_tests().into_bytes()],
+                    },
+                    LdapPartialAttribute {
+                        atype: "last_name".to_string(),
+                        vals: vec!["Böbberson".to_string().into_bytes()],
+                    },
+                    LdapPartialAttribute {
+                        atype: "mail".to_string(),
+                        vals: vec![b"bob@bobmail.bob".to_vec()],
+                    },
+                    LdapPartialAttribute {
+                        atype: "objectclass".to_string(),
+                        vals: vec![
+                            b"inetOrgPerson".to_vec(),
+                            b"posixAccount".to_vec(),
+                            b"mailAccount".to_vec(),
+                            b"person".to_vec(),
+                            b"customUserClass".to_vec(),
+                        ],
+                    },
+                    LdapPartialAttribute {
+                        atype: "sn".to_string(),
+                        vals: vec!["Böbberson".to_string().into_bytes()],
+                    },
+                    LdapPartialAttribute {
+                        atype: "uid".to_string(),
+                        vals: vec![b"bob_1".to_vec()],
+                    },
                 ],
             }),
             // "objectclass", "dn", "uid", "cn", "member", "uniquemember"
@@ -2162,17 +2170,12 @@ mod tests {
                 dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
                 attributes: vec![
                     LdapPartialAttribute {
-                        atype: "objectclass".to_string(),
-                        vals: vec![b"groupOfUniqueNames".to_vec()],
-                    },
-                    // UID
-                    LdapPartialAttribute {
-                        atype: "uid".to_string(),
+                        atype: "cn".to_string(),
                         vals: vec![b"group_1".to_vec()],
                     },
                     LdapPartialAttribute {
-                        atype: "cn".to_string(),
-                        vals: vec![b"group_1".to_vec()],
+                        atype: "entryuuid".to_string(),
+                        vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
                     },
                     //member / uniquemember : "uid={},ou=people,{}"
                     LdapPartialAttribute {
@@ -2182,6 +2185,15 @@ mod tests {
                             b"uid=john,ou=people,dc=example,dc=com".to_vec(),
                         ],
                     },
+                    LdapPartialAttribute {
+                        atype: "objectclass".to_string(),
+                        vals: vec![b"groupOfUniqueNames".to_vec()],
+                    },
+                    // UID
+                    LdapPartialAttribute {
+                        atype: "uid".to_string(),
+                        vals: vec![b"group_1".to_vec()],
+                    },
                     LdapPartialAttribute {
                         atype: "uniquemember".to_string(),
                         vals: vec![
@@ -2189,10 +2201,6 @@ mod tests {
                             b"uid=john,ou=people,dc=example,dc=com".to_vec(),
                         ],
                     },
-                    LdapPartialAttribute {
-                        atype: "entryuuid".to_string(),
-                        vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
-                    },
                 ],
             }),
             make_search_success(),
@@ -2924,27 +2932,27 @@ mod tests {
                 LdapOp::SearchResultEntry(LdapSearchResultEntry {
                     dn: "uid=test,ou=people,dc=example,dc=com".to_string(),
                     attributes: vec![
-                        LdapPartialAttribute {
-                            atype: "uid".to_owned(),
-                            vals: vec![b"test".to_vec()],
-                        },
                         LdapPartialAttribute {
                             atype: "nickname".to_owned(),
                             vals: vec![b"Bob the Builder".to_vec()],
                         },
+                        LdapPartialAttribute {
+                            atype: "uid".to_owned(),
+                            vals: vec![b"test".to_vec()],
+                        },
                     ],
                 }),
                 LdapOp::SearchResultEntry(LdapSearchResultEntry {
                     dn: "cn=group,ou=groups,dc=example,dc=com".to_owned(),
                     attributes: vec![
-                        LdapPartialAttribute {
-                            atype: "uid".to_owned(),
-                            vals: vec![b"group".to_vec()],
-                        },
                         LdapPartialAttribute {
                             atype: "club_name".to_owned(),
                             vals: vec![b"Breakfast Club".to_vec()],
                         },
+                        LdapPartialAttribute {
+                            atype: "uid".to_owned(),
+                            vals: vec![b"group".to_vec()],
+                        },
                     ],
                 }),
                 make_search_success()


Test patch: