[GH-ISSUE #254] ldapsearch result returns duplicate dn attribute #92

Closed
opened 2026-02-27 08:15:12 +03:00 by kerem · 10 comments
Owner

Originally created by @ikaruswill on GitHub (Jul 14, 2022).
Original GitHub issue: https://github.com/lldap/lldap/issues/254

Context

I have a home-assistant installation that relies on an ldap-auth script that runs ldapsearch against an LDAP server by directly binding as logging in user, and running a search on memberof. If the result does not return exactly 1 dn, the login fails. I was just debugging this yesterday and I came across this issue.

Problem

ldapsearch results return duplicate dn attribute, not seen in querying openldap with the exact same query.

Against LLDAP:

$ ldapsearch -H ldap://lldap:389 -LLL -D "uid=admin,ou=people,dc=example,dc=com" -w '<password>' -s "One" -b "ou=people,dc=example,dc=com" '(&(objectClass=person)(memberof=cn=admins,ou=groups,dc=example,dc=com))'
dn: uid=ikaruswill,ou=people,dc=example,dc=com
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: mailAccount
objectclass: person
dn: uid=ikaruswill,ou=people,dc=example,dc=com
uid: ikaruswill
mail: will@example.com
givenname: Will
sn: Ho
cn: Will Ho
createtimestamp: 2022-07-13T00:02:21.017041305+00:00

Against OpenLDAP:

bash-5.1# ldapsearch -H ldap://openldap:389 -LLL -D "cn=admin,dc=example,dc=com" -w '<password>' -s "One" -b "ou=people,dc=example,dc=com" '(&(objectClass=person)(memberof=cn=admins,ou=groups,dc=example,dc=com))'
dn: uid=ikaruswill,ou=people,dc=example,dc=com
objectClass: person
objectClass: inetOrgPerson
objectClass: posixAccount
displayName: Will Ho
uidNumber: 2001
gidNumber: 2001
loginShell: /bin/bash
homeDirectory: /home/will.ho
userPassword:: <password>
givenName: Will
sn: Ho
mail: will@example.com
uid: ikaruswill
cn: Will Ho

Relevant logs:

2022-07-14T00:28:18.938698822+00:00 INFO     LDAP session [ 243ms | 0.11% / 100.00% ]
2022-07-14T00:28:18.938779615+00:00 INFO     ┝━ LDAP request [ 242ms | 0.09% / 99.44% ]
2022-07-14T00:28:18.938804699+00:00 DEBUG    │  ┝━ 🐛 [debug]:  | msg: LdapMsg { msgid: 1, op: BindRequest(LdapBindRequest { dn: "uid=admin,ou=people,dc=example,dc=com", cred: Simple("********") }), ctrl: [] }
2022-07-14T00:28:18.938809949+00:00 DEBUG    │  ┝━ do_bind [ 242ms | 0.02% / 99.35% ]
2022-07-14T00:28:18.938816366+00:00 DEBUG    │  │  ┝━ 🐛 [debug]: DN: uid=admin,ou=people,dc=example,dc=com
2022-07-14T00:28:18.938828325+00:00 DEBUG    │  │  ┝━ bind [ 242ms | 0.08% / 99.23% ]
2022-07-14T00:28:18.939259043+00:00 DEBUG    │  │  │  ┕━ passwords_match [ 241ms | 99.15% ]
2022-07-14T00:28:19.180659806+00:00 DEBUG    │  │  ┝━ get_user_groups [ 239µs | 0.10% ]
2022-07-14T00:28:19.180675848+00:00 DEBUG    │  │  │  ┝━ 🐛 [debug]:  | user_id: UserId("admin")
2022-07-14T00:28:19.180740600+00:00 DEBUG    │  │  │  ┝━ 🐛 [debug]:  | query: SELECT "groups"."group_id", "display_name", "creation_date", "uuid" FROM "groups" INNER JOIN "memberships" ON "groups"."group_id" = "memberships"."group_id" WHERE "user_id" = ?
2022-07-14T00:28:19.180999314+00:00 DEBUG    │  │  │  ┕━ 🐛 [debug]:  | return: {GroupDetails { group_id: GroupId(1), display_name: "lldap_admin", creation_date: 2022-07-12T23:48:25.516387433Z, uuid: Uuid("<UUID>") }}
2022-07-14T00:28:19.181010690+00:00 DEBUG    │  │  ┕━ 🐛 [debug]: Success!
2022-07-14T00:28:19.181025857+00:00 DEBUG    │  ┕━ 🐛 [debug]:  | response: BindResponse(LdapBindResponse { res: LdapResult { code: Success, matcheddn: "", message: "", referral: [] }, saslcreds: None })
2022-07-14T00:28:19.181579370+00:00 INFO     ┝━ LDAP request [ 1.06ms | 0.18% / 0.43% ]
2022-07-14T00:28:19.181692331+00:00 DEBUG    │  ┝━ 🐛 [debug]:  | msg: LdapMsg { msgid: 2, op: SearchRequest(LdapSearchRequest { base: "dc=example,dc=com", scope: Base, aliases: Never, sizelimit: 0, timelimit: 0, typesonly: false, filter: And([Equality("objectClass", "person"), Equality("memberof", "cn=admins,ou=groups,dc=example,dc=com")]), attrs: [] }), ctrl: [] }
2022-07-14T00:28:19.181702247+00:00 DEBUG    │  ┝━ do_search [ 608µs | 0.02% / 0.25% ]
2022-07-14T00:28:19.181720915+00:00 DEBUG    │  │  ┝━ 🐛 [debug]:  | request.base: "dc=example,dc=com" | scope: Global
2022-07-14T00:28:19.181724415+00:00 DEBUG    │  │  ┝━ get_user_list [ 392µs | 0.04% / 0.16% ]
2022-07-14T00:28:19.181734623+00:00 DEBUG    │  │  │  ┝━ 🐛 [debug]:  | ldap_filter: And([Equality("objectClass", "person"), Equality("memberof", "cn=admins,ou=groups,dc=example,dc=com")])
2022-07-14T00:28:19.181751832+00:00 DEBUG    │  │  │  ┝━ 🐛 [debug]:  | parsed_filters: And([And([]), MemberOf("admins")])
2022-07-14T00:28:19.181754457+00:00 DEBUG    │  │  │  ┝━ expand_attribute_wildcards [ 21.3µs | 0.01% ]
2022-07-14T00:28:19.181773999+00:00 DEBUG    │  │  │  │  ┕━ 🐛 [debug]:  | ldap_attributes: [] | resolved_attributes: ["objectclass", "dn", "uid", "mail", "givenname", "sn", "cn", "createtimestamp"]
2022-07-14T00:28:19.181785958+00:00 DEBUG    │  │  │  ┕━ list_users [ 283µs | 0.12% ]
2022-07-14T00:28:19.181793250+00:00 DEBUG    │  │  │     ┝━ 🐛 [debug]:  | filters: Some(And([And([]), MemberOf("admins")])) | get_groups: false
2022-07-14T00:28:19.181891252+00:00 DEBUG    │  │  │     ┝━ 🐛 [debug]:  | query: SELECT "users"."user_id", "email", "users"."display_name", "first_name", "last_name", "avatar", "users"."creation_date", "users"."uuid" FROM "users" LEFT JOIN "memberships" ON "users"."user_id" = "memberships"."user_id" LEFT JOIN "groups" ON "memberships"."group_id" = "groups"."group_id" WHERE ? AND ("groups"."display_name" = ?) ORDER BY "users"."user_id" ASC
2022-07-14T00:28:19.182576976+00:00 DEBUG    │  │  │     ┕━ 🐛 [debug]:  | return: [UserAndGroups { user: User { user_id: UserId("ikaruswill"), email: "will@example.com", display_name: "Will Ho", first_name: "Will", last_name: "Ho", creation_date: 2022-07-13T00:02:21.017041305Z, uuid: Uuid("<UUID>") }, groups: None }]
2022-07-14T00:28:19.182617519+00:00 DEBUG    │  │  ┕━ get_groups_list [ 171µs | 0.02% / 0.07% ]
2022-07-14T00:28:19.182628603+00:00 DEBUG    │  │     ┝━ 🐛 [debug]:  | ldap_filter: And([Equality("objectClass", "person"), Equality("memberof", "cn=admins,ou=groups,dc=example,dc=com")])
2022-07-14T00:28:19.182638811+00:00 WARN     │  │     ┝━ 🚧 [warn]: Ignoring unknown group attribute ""memberof"" in filter.\n\
                                To disable this warning, add it to "ignored_group_attributes" in the config.
2022-07-14T00:28:19.182643770+00:00 DEBUG    │  │     ┝━ 🐛 [debug]:  | parsed_filters: And([Not(And([])), Not(And([]))])
2022-07-14T00:28:19.182646395+00:00 DEBUG    │  │     ┕━ list_groups [ 134µs | 0.05% ]
2022-07-14T00:28:19.182651645+00:00 DEBUG    │  │        ┝━ 🐛 [debug]:  | filters: Some(And([Not(And([])), Not(And([]))]))
2022-07-14T00:28:19.182708521+00:00 DEBUG    │  │        ┝━ 🐛 [debug]:  | query: SELECT "groups"."group_id", "display_name", "creation_date", "uuid", "user_id" FROM "groups" LEFT JOIN "memberships" ON "groups"."group_id" = "memberships"."group_id" WHERE NOT ? AND NOT ? ORDER BY "display_name" ASC, "user_id" ASC
2022-07-14T00:28:19.183277285+00:00 DEBUG    │  │        ┕━ 🐛 [debug]:  | return: []
2022-07-14T00:28:19.183326286+00:00 DEBUG    │  ┝━ 🐛 [debug]:  | response: SearchResultEntry(LdapSearchResultEntry { dn: "uid=ikaruswill,ou=people,dc=example,dc=com", attributes: [LdapPartialAttribute { atype: "objectclass", vals: ["inetOrgPerson", "posixAccount", "mailAccount", "person"] }, LdapPartialAttribute { atype: "dn", vals: ["uid=ikaruswill,ou=people,dc=example,dc=com"] }, LdapPartialAttribute { atype: "uid", vals: ["ikaruswill"] }, LdapPartialAttribute { atype: "mail", vals: ["will@example.com"] }, LdapPartialAttribute { atype: "givenname", vals: ["Will"] }, LdapPartialAttribute { atype: "sn", vals: ["Ho"] }, LdapPartialAttribute { atype: "cn", vals: ["Will Ho"] }, LdapPartialAttribute { atype: "createtimestamp", vals: ["2022-07-13T00:02:21.017041305+00:00"] }] })
2022-07-14T00:28:19.183550583+00:00 DEBUG    │  ┕━ 🐛 [debug]:  | response: SearchResultDone(LdapResult { code: Success, matcheddn: "", message: "", referral: [] })
2022-07-14T00:28:19.184405769+00:00 INFO     ┕━ LDAP request [ 22.8µs | 0.01% ]
2022-07-14T00:28:19.184424728+00:00 DEBUG       ┕━ 🐛 [debug]:  | msg: LdapMsg { msgid: 3, op: UnbindRequest, ctrl: [] }
Originally created by @ikaruswill on GitHub (Jul 14, 2022). Original GitHub issue: https://github.com/lldap/lldap/issues/254 ## Context I have a home-assistant installation that relies on an `ldap-auth` script that runs `ldapsearch` against an LDAP server by directly binding as logging in user, and running a search on `memberof`. If the result does not return exactly 1 `dn`, the login fails. I was just debugging this yesterday and I came across this issue. ## Problem `ldapsearch` results return duplicate `dn` attribute, not seen in querying `openldap` with the exact same query. Against LLDAP: ``` $ ldapsearch -H ldap://lldap:389 -LLL -D "uid=admin,ou=people,dc=example,dc=com" -w '<password>' -s "One" -b "ou=people,dc=example,dc=com" '(&(objectClass=person)(memberof=cn=admins,ou=groups,dc=example,dc=com))' dn: uid=ikaruswill,ou=people,dc=example,dc=com objectclass: inetOrgPerson objectclass: posixAccount objectclass: mailAccount objectclass: person dn: uid=ikaruswill,ou=people,dc=example,dc=com uid: ikaruswill mail: will@example.com givenname: Will sn: Ho cn: Will Ho createtimestamp: 2022-07-13T00:02:21.017041305+00:00 ``` Against OpenLDAP: ``` bash-5.1# ldapsearch -H ldap://openldap:389 -LLL -D "cn=admin,dc=example,dc=com" -w '<password>' -s "One" -b "ou=people,dc=example,dc=com" '(&(objectClass=person)(memberof=cn=admins,ou=groups,dc=example,dc=com))' dn: uid=ikaruswill,ou=people,dc=example,dc=com objectClass: person objectClass: inetOrgPerson objectClass: posixAccount displayName: Will Ho uidNumber: 2001 gidNumber: 2001 loginShell: /bin/bash homeDirectory: /home/will.ho userPassword:: <password> givenName: Will sn: Ho mail: will@example.com uid: ikaruswill cn: Will Ho ``` Relevant logs: ``` 2022-07-14T00:28:18.938698822+00:00 INFO LDAP session [ 243ms | 0.11% / 100.00% ] 2022-07-14T00:28:18.938779615+00:00 INFO ┝━ LDAP request [ 242ms | 0.09% / 99.44% ] 2022-07-14T00:28:18.938804699+00:00 DEBUG │ ┝━ 🐛 [debug]: | msg: LdapMsg { msgid: 1, op: BindRequest(LdapBindRequest { dn: "uid=admin,ou=people,dc=example,dc=com", cred: Simple("********") }), ctrl: [] } 2022-07-14T00:28:18.938809949+00:00 DEBUG │ ┝━ do_bind [ 242ms | 0.02% / 99.35% ] 2022-07-14T00:28:18.938816366+00:00 DEBUG │ │ ┝━ 🐛 [debug]: DN: uid=admin,ou=people,dc=example,dc=com 2022-07-14T00:28:18.938828325+00:00 DEBUG │ │ ┝━ bind [ 242ms | 0.08% / 99.23% ] 2022-07-14T00:28:18.939259043+00:00 DEBUG │ │ │ ┕━ passwords_match [ 241ms | 99.15% ] 2022-07-14T00:28:19.180659806+00:00 DEBUG │ │ ┝━ get_user_groups [ 239µs | 0.10% ] 2022-07-14T00:28:19.180675848+00:00 DEBUG │ │ │ ┝━ 🐛 [debug]: | user_id: UserId("admin") 2022-07-14T00:28:19.180740600+00:00 DEBUG │ │ │ ┝━ 🐛 [debug]: | query: SELECT "groups"."group_id", "display_name", "creation_date", "uuid" FROM "groups" INNER JOIN "memberships" ON "groups"."group_id" = "memberships"."group_id" WHERE "user_id" = ? 2022-07-14T00:28:19.180999314+00:00 DEBUG │ │ │ ┕━ 🐛 [debug]: | return: {GroupDetails { group_id: GroupId(1), display_name: "lldap_admin", creation_date: 2022-07-12T23:48:25.516387433Z, uuid: Uuid("<UUID>") }} 2022-07-14T00:28:19.181010690+00:00 DEBUG │ │ ┕━ 🐛 [debug]: Success! 2022-07-14T00:28:19.181025857+00:00 DEBUG │ ┕━ 🐛 [debug]: | response: BindResponse(LdapBindResponse { res: LdapResult { code: Success, matcheddn: "", message: "", referral: [] }, saslcreds: None }) 2022-07-14T00:28:19.181579370+00:00 INFO ┝━ LDAP request [ 1.06ms | 0.18% / 0.43% ] 2022-07-14T00:28:19.181692331+00:00 DEBUG │ ┝━ 🐛 [debug]: | msg: LdapMsg { msgid: 2, op: SearchRequest(LdapSearchRequest { base: "dc=example,dc=com", scope: Base, aliases: Never, sizelimit: 0, timelimit: 0, typesonly: false, filter: And([Equality("objectClass", "person"), Equality("memberof", "cn=admins,ou=groups,dc=example,dc=com")]), attrs: [] }), ctrl: [] } 2022-07-14T00:28:19.181702247+00:00 DEBUG │ ┝━ do_search [ 608µs | 0.02% / 0.25% ] 2022-07-14T00:28:19.181720915+00:00 DEBUG │ │ ┝━ 🐛 [debug]: | request.base: "dc=example,dc=com" | scope: Global 2022-07-14T00:28:19.181724415+00:00 DEBUG │ │ ┝━ get_user_list [ 392µs | 0.04% / 0.16% ] 2022-07-14T00:28:19.181734623+00:00 DEBUG │ │ │ ┝━ 🐛 [debug]: | ldap_filter: And([Equality("objectClass", "person"), Equality("memberof", "cn=admins,ou=groups,dc=example,dc=com")]) 2022-07-14T00:28:19.181751832+00:00 DEBUG │ │ │ ┝━ 🐛 [debug]: | parsed_filters: And([And([]), MemberOf("admins")]) 2022-07-14T00:28:19.181754457+00:00 DEBUG │ │ │ ┝━ expand_attribute_wildcards [ 21.3µs | 0.01% ] 2022-07-14T00:28:19.181773999+00:00 DEBUG │ │ │ │ ┕━ 🐛 [debug]: | ldap_attributes: [] | resolved_attributes: ["objectclass", "dn", "uid", "mail", "givenname", "sn", "cn", "createtimestamp"] 2022-07-14T00:28:19.181785958+00:00 DEBUG │ │ │ ┕━ list_users [ 283µs | 0.12% ] 2022-07-14T00:28:19.181793250+00:00 DEBUG │ │ │ ┝━ 🐛 [debug]: | filters: Some(And([And([]), MemberOf("admins")])) | get_groups: false 2022-07-14T00:28:19.181891252+00:00 DEBUG │ │ │ ┝━ 🐛 [debug]: | query: SELECT "users"."user_id", "email", "users"."display_name", "first_name", "last_name", "avatar", "users"."creation_date", "users"."uuid" FROM "users" LEFT JOIN "memberships" ON "users"."user_id" = "memberships"."user_id" LEFT JOIN "groups" ON "memberships"."group_id" = "groups"."group_id" WHERE ? AND ("groups"."display_name" = ?) ORDER BY "users"."user_id" ASC 2022-07-14T00:28:19.182576976+00:00 DEBUG │ │ │ ┕━ 🐛 [debug]: | return: [UserAndGroups { user: User { user_id: UserId("ikaruswill"), email: "will@example.com", display_name: "Will Ho", first_name: "Will", last_name: "Ho", creation_date: 2022-07-13T00:02:21.017041305Z, uuid: Uuid("<UUID>") }, groups: None }] 2022-07-14T00:28:19.182617519+00:00 DEBUG │ │ ┕━ get_groups_list [ 171µs | 0.02% / 0.07% ] 2022-07-14T00:28:19.182628603+00:00 DEBUG │ │ ┝━ 🐛 [debug]: | ldap_filter: And([Equality("objectClass", "person"), Equality("memberof", "cn=admins,ou=groups,dc=example,dc=com")]) 2022-07-14T00:28:19.182638811+00:00 WARN │ │ ┝━ 🚧 [warn]: Ignoring unknown group attribute ""memberof"" in filter.\n\ To disable this warning, add it to "ignored_group_attributes" in the config. 2022-07-14T00:28:19.182643770+00:00 DEBUG │ │ ┝━ 🐛 [debug]: | parsed_filters: And([Not(And([])), Not(And([]))]) 2022-07-14T00:28:19.182646395+00:00 DEBUG │ │ ┕━ list_groups [ 134µs | 0.05% ] 2022-07-14T00:28:19.182651645+00:00 DEBUG │ │ ┝━ 🐛 [debug]: | filters: Some(And([Not(And([])), Not(And([]))])) 2022-07-14T00:28:19.182708521+00:00 DEBUG │ │ ┝━ 🐛 [debug]: | query: SELECT "groups"."group_id", "display_name", "creation_date", "uuid", "user_id" FROM "groups" LEFT JOIN "memberships" ON "groups"."group_id" = "memberships"."group_id" WHERE NOT ? AND NOT ? ORDER BY "display_name" ASC, "user_id" ASC 2022-07-14T00:28:19.183277285+00:00 DEBUG │ │ ┕━ 🐛 [debug]: | return: [] 2022-07-14T00:28:19.183326286+00:00 DEBUG │ ┝━ 🐛 [debug]: | response: SearchResultEntry(LdapSearchResultEntry { dn: "uid=ikaruswill,ou=people,dc=example,dc=com", attributes: [LdapPartialAttribute { atype: "objectclass", vals: ["inetOrgPerson", "posixAccount", "mailAccount", "person"] }, LdapPartialAttribute { atype: "dn", vals: ["uid=ikaruswill,ou=people,dc=example,dc=com"] }, LdapPartialAttribute { atype: "uid", vals: ["ikaruswill"] }, LdapPartialAttribute { atype: "mail", vals: ["will@example.com"] }, LdapPartialAttribute { atype: "givenname", vals: ["Will"] }, LdapPartialAttribute { atype: "sn", vals: ["Ho"] }, LdapPartialAttribute { atype: "cn", vals: ["Will Ho"] }, LdapPartialAttribute { atype: "createtimestamp", vals: ["2022-07-13T00:02:21.017041305+00:00"] }] }) 2022-07-14T00:28:19.183550583+00:00 DEBUG │ ┕━ 🐛 [debug]: | response: SearchResultDone(LdapResult { code: Success, matcheddn: "", message: "", referral: [] }) 2022-07-14T00:28:19.184405769+00:00 INFO ┕━ LDAP request [ 22.8µs | 0.01% ] 2022-07-14T00:28:19.184424728+00:00 DEBUG ┕━ 🐛 [debug]: | msg: LdapMsg { msgid: 3, op: UnbindRequest, ctrl: [] } ```
kerem 2026-02-27 08:15:12 +03:00
Author
Owner

@nitnelave commented on GitHub (Jul 14, 2022):

Oh, I see, dn is returned as part of the search result and as an attribute. That's easy to fix :)

<!-- gh-comment-id:1184013003 --> @nitnelave commented on GitHub (Jul 14, 2022): Oh, I see, dn is returned as part of the search result and as an attribute. That's easy to fix :)
Author
Owner

@ikaruswill commented on GitHub (Jul 14, 2022):

For reference:
https://github.com/bob1de/ldap-auth-sh/blob/master/ldap-auth.sh

Myself and I believe quite a number of other users use this script to integrate LDAP authentication into home-assistant.

<!-- gh-comment-id:1184037045 --> @ikaruswill commented on GitHub (Jul 14, 2022): For reference: https://github.com/bob1de/ldap-auth-sh/blob/master/ldap-auth.sh Myself and I believe quite a number of other users use this script to integrate LDAP authentication into home-assistant.
Author
Owner

@ikaruswill commented on GitHub (Oct 10, 2022):

Hi there @nitnelave any chance you'll be pushing a new tagged release that includes this change soon?

<!-- gh-comment-id:1273439781 --> @ikaruswill commented on GitHub (Oct 10, 2022): Hi there @nitnelave any chance you'll be pushing a new tagged release that includes this change soon?
Author
Owner

@nitnelave commented on GitHub (Oct 10, 2022):

Sure, let me prepare a minor release.

<!-- gh-comment-id:1273486359 --> @nitnelave commented on GitHub (Oct 10, 2022): Sure, let me prepare a minor release.
Author
Owner

@ikaruswill commented on GitHub (Oct 10, 2022):

On this issue, I just bumped my LLDAP version to 0.4.1 and I'm afraid there was another problem.

Issue

It appears that dexidp/dex project requires some predefined attribute to be present on the Person object in order to match a Group's list of member attributes' values to determine group membership. Removing the dn attribute from the Person object essentially disables this matching process here as there is no dn attribute to access in getAttrs(), so the auth process silently fails.

Context

For context, I'm using dex as an auth connector for argoproj/argo-cd, and dex itself is a widely used auth connector with increasing adoption.

Why did it work on OpenLDAP

As for why it used to work in OpenLDAP, it can probably be explained by the RFC2307bis schema used in OpenLDAP in the past where the memberUID attribute on the Person was used as a member attribute value on the Group, hence matching was no issue. But since LLDAP does not follow the RFC2307bis schema, it indirectly gave rise to this issue.

Options

  • Revert this change and consequently have the home-assistant community to rethink authentication via the ldap-auth.sh script if they were to come across the same issue.
  • Use the uid of the Person object as value of member attributes in Group objects, which is a breaking change.

What are your thoughts? @nitnelave

<!-- gh-comment-id:1273636148 --> @ikaruswill commented on GitHub (Oct 10, 2022): On this issue, I just bumped my LLDAP version to `0.4.1` and I'm afraid there was another problem. ## Issue It appears that [dexidp/dex](https://github.com/dexidp/dex) project [requires some predefined attribute](https://dexidp.io/docs/connectors/ldap/) to be present on the `Person` object in order to match a `Group`'s list of `member` attributes' values to determine group membership. Removing the `dn` attribute from the `Person` object essentially disables this matching process [here](https://github.com/dexidp/dex/blob/master/connector/ldap/ldap.go#L584) as there is no `dn` attribute to access in `getAttrs()`, so the auth process silently fails. ## Context For context, I'm using dex as an auth connector for [argoproj/argo-cd](https://github.com/argoproj/argo-cd), and dex itself is a widely used auth connector with increasing adoption. ## Why did it work on OpenLDAP As for why it used to work in OpenLDAP, it can probably be explained by the [`RFC2307bis`](https://unofficialaciguide.com/2019/07/31/ldap-schemas-for-aci-administrators-rfc2307-vs-rfc2307bis/) schema used in OpenLDAP in the past where the `memberUID` attribute on the `Person` was used as a `member` attribute value on the `Group`, hence matching was no issue. But since LLDAP does not follow the `RFC2307bis` schema, it indirectly gave rise to this issue. ## Options - Revert this change and consequently have the `home-assistant` community to rethink authentication via the [ldap-auth.sh](https://github.com/bob1de/ldap-auth-sh/blob/master/ldap-auth.sh) script if they were to come across the same issue. - Use the `uid` of the `Person` object as value of `member` attributes in `Group` objects, which is a **breaking change**. What are your thoughts? @nitnelave
Author
Owner

@nitnelave commented on GitHub (Oct 10, 2022):

A small starting note: I made the initial change not just because it fixes the automation script, but also because it makes sense on its own for the project. I don't think reverting is the way to go.

On RFC2307 vs 2307bis: as I see it, the difference is not between member and memberUID, but between member (attribute of groups) and memberOf (attribute of user). LLDAP implements both :)
A non-breaking change going forward can be to add the memberUID attribute to the groups, that identifies users based on their UID instead of DN.

It seems you can also configure Dex to use CN or UID instead of DN, in the configuration:

User entries are expected to have an email attribute (configurable through emailAttr), and a display name attribute (configurable through nameAttr).

Regarding the group search, they say:
# Following list contains field pairs that are used to match a user to a group. It adds an additional
# requirement to the filter that an attribute in the group must match the user's
# attribute value.
userMatchers:
- userAttr: uid
groupAttr: member

It seems that setting the userAttr to dn should work? And if their getAttr function doesn't return anything for the dn, I'd say that it's a bug on their part. If they don't want to fix it, we might look into returning it as an attribute IFF it's requested as an attribute, but I'm a bit skeptical that it's the right way forward.

<!-- gh-comment-id:1273715867 --> @nitnelave commented on GitHub (Oct 10, 2022): A small starting note: I made the initial change not just because it fixes the automation script, but also because it makes sense on its own for the project. I don't think reverting is the way to go. On RFC2307 vs 2307bis: as I see it, the difference is not between `member` and `memberUID`, but between `member` (attribute of groups) and `memberOf` (attribute of user). LLDAP implements both :) A non-breaking change going forward can be to add the `memberUID` attribute to the groups, that identifies users based on their UID instead of DN. It seems you can also configure Dex to use `CN` or `UID` instead of `DN`, in the configuration: > User entries are expected to have an email attribute (configurable through `emailAttr`), and a display name attribute (configurable through `nameAttr`). Regarding the group search, they say: # Following list contains field pairs that are used to match a user to a group. It adds an additional # requirement to the filter that an attribute in the group must match the user's # attribute value. userMatchers: - userAttr: uid groupAttr: member It seems that setting the `userAttr` to `dn` should work? And if their `getAttr` function doesn't return anything for the `dn`, I'd say that it's a bug on their part. If they don't want to fix it, we might look into returning it as an attribute IFF it's requested as an attribute, but I'm a bit skeptical that it's the right way forward.
Author
Owner

@ikaruswill commented on GitHub (Oct 11, 2022):

Hmm interesting, thanks for taking a deep look into this.

I had thought that the change in #281 was to remove the dn attribute on Person returned when executing a search. Based on what you're saying and after taking a closer look at the PR, it seems the dn attribute is still returned as a default behaviour, just that an extra dn return was removed. My bad.

Fully agree with your response overall. Let me dive a little deeper into their code to see what's going on with the getAttrs function. If all else fails, I'll file another issue as a feature request for the memberUID attribute on Group object.

Really appreciate your time!

<!-- gh-comment-id:1274735678 --> @ikaruswill commented on GitHub (Oct 11, 2022): Hmm interesting, thanks for taking a deep look into this. I had thought that the change in #281 was to remove the `dn` attribute on `Person` returned when executing a search. Based on what you're saying and after taking a closer look at the PR, it seems the `dn` attribute is still returned as a default behaviour, just that an extra `dn` return was removed. My bad. Fully agree with your response overall. Let me dive a little deeper into their code to see what's going on with the `getAttrs` function. If all else fails, I'll file another issue as a feature request for the `memberUID` attribute on `Group` object. Really appreciate your time!
Author
Owner

@ikaruswill commented on GitHub (Oct 11, 2022):

Update:

After digging deeper, I found a bug in dex's getAttrs function.

In the underlying dependency package go-ldap, the dn attribute is treated in a special way and is not considered an "attribute" of the ldap.Entry object so it cannot be found in ldap.Entry.Attributes.

go-ldap's ldap.Entry type (Ref):

// Entry represents a single search result entry
type Entry struct {
	// DN is the distinguished name of the entry
	DN string
	// Attributes are the returned attributes for the entry
	Attributes []*EntryAttribute
}

And here is where the dn is parsed and its value inserted into the ldap.Entry.DN attribute.

go-ldap's ldap.Entry.Unmarshal method (Ref):

...
                // Fill the field with the distinguishedName if the tag key is `dn`
		if fieldTag == "dn" {
			fv.SetString(e.DN)
			continue
		}
...

This creates an edge case, so getAttrs has to handle for when dn is specified as the userAttr for matching, where it must retrieve the dn value from ldap.Entry.DN itself rather than from the list in ldap.Entry.Attributes.

Back to dex, in the getAttrs function, we can see how they handle the edge case.

func getAttrs(e ldap.Entry, name string) []string {
	for _, a := range e.Attributes {
		if a.Name != name {
			continue
		}
		return a.Values
	}
	if name == "DN" {
		return []string{e.DN}
	}
	return nil
}

But this if name == "DN" block uses direct string matching and does not account for when the configuration specifies the lower-cased value "dn". Thus after switching the userAttr to "DN", it worked flawlessly.

I initially specified dn as the userAttr and it just didn't work with lldap:0.4.1 since the duplicate dn attribute was removed. My guess as to why it worked prior to the latest changes is that only the first dn was processed separately, and the duplicate dn was accepted as an attribute of ldap.Entry, tbh I never really got down to figuring out why.

Nonetheless, thanks for pointing me in the right direction. I'll go ahead and submit a PR to dex to do a case-insensitive match on the value "DN".

<!-- gh-comment-id:1274807006 --> @ikaruswill commented on GitHub (Oct 11, 2022): Update: After digging deeper, I found a bug in `dex`'s `getAttrs` function. In the underlying dependency package `go-ldap`, the `dn` attribute is treated in a special way and is not considered an "attribute" of the `ldap.Entry` object so it cannot be found in `ldap.Entry.Attributes`. `go-ldap`'s `ldap.Entry` type ([Ref](https://github.com/go-ldap/ldap/blob/c7248aadbf1fc7c0e97119fef902c0ec494e8fc5/search.go#L65)): ``` // Entry represents a single search result entry type Entry struct { // DN is the distinguished name of the entry DN string // Attributes are the returned attributes for the entry Attributes []*EntryAttribute } ``` And here is where the `dn` is parsed and its value inserted into the `ldap.Entry.DN` attribute. `go-ldap`'s `ldap.Entry.Unmarshal` method ([Ref](https://github.com/go-ldap/ldap/blob/c7248aadbf1fc7c0e97119fef902c0ec494e8fc5/search.go#L252)): ``` ... // Fill the field with the distinguishedName if the tag key is `dn` if fieldTag == "dn" { fv.SetString(e.DN) continue } ... ``` This creates an edge case, so `getAttrs` has to handle for when `dn` is specified as the `userAttr` for matching, where it must retrieve the `dn` value from `ldap.Entry.DN` itself rather than from the list in `ldap.Entry.Attributes`. Back to `dex`, in the `getAttrs` function, we can see how they handle the edge case. ``` func getAttrs(e ldap.Entry, name string) []string { for _, a := range e.Attributes { if a.Name != name { continue } return a.Values } if name == "DN" { return []string{e.DN} } return nil } ``` But this `if name == "DN"` block uses direct string matching and does not account for when the configuration specifies the lower-cased value "dn". Thus after switching the `userAttr` to "DN", it worked flawlessly. I initially specified `dn` as the `userAttr` and it just didn't work with `lldap:0.4.1` since the duplicate `dn` attribute was removed. My guess as to why it worked prior to the latest changes is that only the first `dn` was processed separately, and the duplicate `dn` was accepted as an attribute of `ldap.Entry`, tbh I never really got down to figuring out why. Nonetheless, thanks for pointing me in the right direction. I'll go ahead and submit a PR to dex to do a case-insensitive match on the value "DN".
Author
Owner

@nitnelave commented on GitHub (Oct 11, 2022):

I'll just mention that their documentation has a pretty explicit paragraph about "DN" being case-sensitive :) But yeah, I think it's better to have a case-insensitive "DN" (actuallly every attribute should be case insensitive according to the LDAP RFCs).

Good job with the investigation!

<!-- gh-comment-id:1274813139 --> @nitnelave commented on GitHub (Oct 11, 2022): I'll just mention that their documentation has a pretty explicit paragraph about "DN" being case-sensitive :) But yeah, I think it's better to have a case-insensitive "DN" (actuallly every attribute should be case insensitive according to the LDAP RFCs). Good job with the investigation!
Author
Owner

@ikaruswill commented on GitHub (Oct 11, 2022):

Hah! I can't believe I missed that! 🤦‍♂️ Thanks for the heads up.

<!-- gh-comment-id:1274822441 --> @ikaruswill commented on GitHub (Oct 11, 2022): Hah! I can't believe I missed that! 🤦‍♂️ Thanks for the heads up.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/lldap-lldap#92
No description provided.