Repository: lldap/lldap (language: rust)

Issue:
[FEATURE REQUEST] Add entrydn as an attribute for groups to permit Duo directory synchronisation
**Motivation**
In order to implement Duo's directory sync via their authentication proxy service, they require the following

> Copied from https://duo.com/docs/ldapsync#prerequisites:
> 
>     Synced groups must have the groupOfNames object class.
>     Synced groups must list their members by DN (directoryName) via themember attribute.
>     Synced groups must have a cn attribute, used as the Duo group name after import.
>     Synced users must have the organizationalPerson object class.
> Users:
> 
>     Synced users or admins must list group memberships by DN using the memberOf attribute (via the [memberOf overlay](http://www.openldap.org/software/man.cgi?query=slapo-memberof&sektion=5&apropos=0&manpath=OpenLDAP+2.4-Release)).
>     Synced users or admins must have the organizationalPerson object class.
>     Synced groups must also have the attributes entrydn (used as the distinguished name) and entryuuid (the user unique identifier).

**Describe the solution you'd like**
Implement entrydn as a group attribute plus any from above required to get Duo directory sync to function.

**Describe alternatives you've considered**
No alternative available as the requirements are specified by Duo

**Additional context**
Duo's OpenLDAP Synchronization
Duo imports users and administrators via LDAP from OpenLDAP directories. When configuring OpenLDAP sync, you'll need to install the Duo Authentication Proxy application on a server that can connect to your directory server.
![image](https://github.com/lldap/lldap/assets/112524737/c0c09399-3c78-4e6f-9941-d64ae4cc76e7)

The above could potentially allow implementation of 2fa for services like jellyfin without breaking access to the mobile/TV app (example workflow given below). While Authelia can still be used to control/add/delete entries in LLDAP.

![image](https://github.com/lldap/lldap/assets/112524737/a87fee8f-c901-4a4e-bb03-8f6b5f624ef2)




Discussion / hints:

Fix patch:
diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs
index ff28abdf..9fe15de0 100644
--- a/server/src/domain/ldap/group.rs
+++ b/server/src/domain/ldap/group.rs
@@ -31,6 +31,9 @@ pub fn get_group_attribute(
         "objectclass" => vec![b"groupOfUniqueNames".to_vec()],
         // Always returned as part of the base response.
         "dn" | "distinguishedname" => return None,
+        "entrydn" => {
+            vec![format!("uid={},ou=groups,{}", group.display_name, base_dn_str).into_bytes()]
+        }
         "cn" | "uid" | "id" => vec![group.display_name.to_string().into_bytes()],
         "entryuuid" | "uuid" => vec![group.uuid.to_string().into_bytes()],
         "member" | "uniquemember" => group
diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs
index 5306fde5..1ffce50e 100644
--- a/server/src/domain/ldap/user.rs
+++ b/server/src/domain/ldap/user.rs
@@ -35,6 +35,9 @@ pub fn get_user_attribute(
         ],
         // dn is always returned as part of the base response.
         "dn" | "distinguishedname" => return None,
+        "entrydn" => {
+            vec![format!("uid={},ou=people,{}", &user.user_id, base_dn_str).into_bytes()]
+        }
         "uid" | "user_id" | "id" => vec![user.user_id.to_string().into_bytes()],
         "entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()],
         "mail" | "email" => vec![user.email.to_string().into_bytes()],
diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs
index 5e6624a6..cc0666b5 100644
--- a/server/src/infra/ldap_handler.rs
+++ b/server/src/infra/ldap_handler.rs
@@ -1368,7 +1368,14 @@ mod tests {
         let mut ldap_handler = setup_bound_admin_handler(mock).await;
         let request = make_group_search_request(
             LdapFilter::And(vec![]),
-            vec!["objectClass", "dn", "cn", "uniqueMember", "entryUuid"],
+            vec![
+                "objectClass",
+                "dn",
+                "cn",
+                "uniqueMember",
+                "entryUuid",
+                "entryDN",
+            ],
         );
         assert_eq!(
             ldap_handler.do_search_or_dse(&request).await,
@@ -1395,6 +1402,10 @@ mod tests {
                             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()],
+                        },
                     ],
                 }),
                 LdapOp::SearchResultEntry(LdapSearchResultEntry {
@@ -1416,6 +1427,10 @@ mod tests {
                             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()],
+                        },
                     ],
                 }),
                 make_search_success(),


Test patch: