From 7d846b2060b7db46ba5584f15b1174be89e9c4aa Mon Sep 17 00:00:00 2001 From: Xiao Mao <96295722+GZYZhy@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:23:25 +0800 Subject: [PATCH] feat: implement root DSE handling and schema query in LDAP server (#4020) --- ldap/server.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ ldap/util.go | 23 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/ldap/server.go b/ldap/server.go index e3f1d12b..0f0f4181 100644 --- a/ldap/server.go +++ b/ldap/server.go @@ -159,6 +159,11 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) { default: } + if strings.EqualFold(r.FilterString(), "(objectClass=*)") && (string(r.BaseObject()) == "" || strings.EqualFold(string(r.BaseObject()), "cn=Subschema")) { + handleRootSearch(w, &r, &res, m) + return + } + var isGroupSearch bool = false filter := r.Filter() if eq, ok := filter.(message.FilterEqualityMatch); ok && strings.EqualFold(string(eq.AttributeDesc()), "objectClass") && strings.EqualFold(string(eq.AssertionValue()), "posixGroup") { @@ -229,6 +234,46 @@ func handleSearch(w ldap.ResponseWriter, m *ldap.Message) { w.Write(res) } +func handleRootSearch(w ldap.ResponseWriter, r *message.SearchRequest, res *message.SearchResultDone, m *ldap.Message) { + if len(r.Attributes()) == 0 { + w.Write(res) + return + } + firstAttr := string(r.Attributes()[0]) + + if string(r.BaseObject()) == "" { + // Handle special root DSE requests + if strings.EqualFold(firstAttr, "namingContexts") { + orgs, code := GetFilteredOrganizations(m) + if code != ldap.LDAPResultSuccess { + res.SetResultCode(code) + w.Write(res) + return + } + e := ldap.NewSearchResultEntry(string(r.BaseObject())) + dnlist := make([]message.AttributeValue, len(orgs)) + for i, org := range orgs { + dnlist[i] = message.AttributeValue(fmt.Sprintf("ou=%s", org.Name)) + } + e.AddAttribute("namingContexts", dnlist...) + w.Write(e) + } else if strings.EqualFold(firstAttr, "subschemaSubentry") { + e := ldap.NewSearchResultEntry(string(r.BaseObject())) + e.AddAttribute("subschemaSubentry", message.AttributeValue("cn=Subschema")) + w.Write(e) + } + } else if strings.EqualFold(firstAttr, "objectclasses") && strings.EqualFold(string(r.BaseObject()), "cn=Subschema") { + e := ldap.NewSearchResultEntry(string(r.BaseObject())) + e.AddAttribute("objectClasses", []message.AttributeValue{ + "( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) )", + "( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) )", + }...) + w.Write(e) + } + + w.Write(res) +} + func hash(s string) uint32 { h := fnv.New32a() h.Write([]byte(s)) diff --git a/ldap/util.go b/ldap/util.go index 254acaaf..7e92ffc9 100644 --- a/ldap/util.go +++ b/ldap/util.go @@ -358,6 +358,29 @@ func GetFilteredGroups(m *ldap.Message, baseDN string, filterStr string) ([]*obj return groups, ldap.LDAPResultSuccess } +func GetFilteredOrganizations(m *ldap.Message) ([]*object.Organization, int) { + if m.Client.IsGlobalAdmin { + organizations, err := object.GetOrganizations("") + if err != nil { + panic(err) + } + return organizations, ldap.LDAPResultSuccess + } else if m.Client.IsOrgAdmin { + requestUserId := util.GetId(m.Client.OrgName, m.Client.UserName) + user, err := object.GetUser(requestUserId) + if err != nil { + panic(err) + } + organization, err := object.GetOrganizationByUser(user) + if err != nil { + panic(err) + } + return []*object.Organization{organization}, ldap.LDAPResultSuccess + } else { + return nil, ldap.LDAPResultInsufficientAccessRights + } +} + // get user password with hash type prefix // TODO not handle salt yet // @return {md5}5f4dcc3b5aa765d61d8327deb882cf99