feat(ldap): ad/openldap dual compat and config gating

- multi-round user filters (config filter first, then
  objectClass=inetOrgPerson, then AD sAMAccountName/userPrincipalName
  fallback) applied to login search, user listing and batch import;
  previously pure-AD directories found no users
- attribute fallback chains (uid/sAMAccountName/userPrincipalName,
  cn/displayName, telephoneNumber/mobile) via ldapEntryAttr
- import errors explain likely cause instead of empty result
- AES-encrypted admin password kept (aiops stores plaintext)
- frontend gating per aiops pattern: LDAP account management panel
  hidden with hint until a config is actually saved (was gated on
  unsaved form values); batch-import modal hides to/from-LDAP tabs
  when unconfigured, local CSV always available
This commit is contained in:
Logan.Li committed 2026-10-26 20:08:46 +08:00
1 parent 2c96902fbd
commit 13fb3aceb3
6 files changed
+487 -266

No files matched your search

+51 -50
View File
@@ -333,41 +333,33 @@ func ListLDAPUsers(c *gin.Context) {
searchBase = req.OuDn
}
baseFilter := "(objectClass=inetOrgPerson)"
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
baseFilter = strings.ReplaceAll(sf, "%s", "*")
} else {
baseFilter = sf
}
}
sr, err := conn.Search(ldap.NewSearchRequest(
searchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
200, 0, false, baseFilter,
[]string{"uid", "cn", "mail", "dn"},
nil,
))
if err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
// 多轮过滤器(配置优先 → OpenLDAP → AD 兜底),命中后去重合并
type LdapUser struct {
DN string `json:"dn"`
UID string `json:"uid"`
CN string `json:"cn"`
Mail string `json:"mail"`
}
seen := map[string]bool{}
var users []LdapUser
for _, e := range sr.Entries {
users = append(users, LdapUser{
DN: e.DN,
UID: e.GetAttributeValue("uid"),
CN: e.GetAttributeValue("cn"),
Mail: e.GetAttributeValue("mail"),
})
for _, filter := range ldapUserListFilters(&cfg) {
sr, err := conn.Search(ldap.NewSearchRequest(
searchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
200, 0, false, filter,
[]string{"uid", "sAMAccountName", "userPrincipalName", "cn", "displayName", "mail", "dn"},
nil,
))
if err != nil {
continue
}
for _, e := range sr.Entries {
if seen[e.DN] {
continue
}
seen[e.DN] = true
uid, cn, mail := ldapIdentityFromEntry(e, "")
users = append(users, LdapUser{DN: e.DN, UID: uid, CN: cn, Mail: mail})
}
}
c.JSON(http.StatusOK, users)
}
@@ -531,23 +523,35 @@ func BatchImportLDAP(c *gin.Context) {
searchBase = req.OuDn
}
baseFilter := "(objectClass=inetOrgPerson)"
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
baseFilter = strings.ReplaceAll(sf, "%s", "*")
} else {
baseFilter = sf
// 多轮过滤器搜索(配置优先 → OpenLDAP → AD 兜底),按 DN 去重合并
type ldapEntryUser struct {
uid, cn, mail string
}
var candidates []ldapEntryUser
seen := map[string]bool{}
for _, filter := range ldapUserListFilters(&cfg) {
sr, err := conn.Search(ldap.NewSearchRequest(
searchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
500, 0, false, filter,
[]string{"uid", "sAMAccountName", "userPrincipalName", "cn", "displayName", "mail"}, nil,
))
if err != nil {
continue
}
for _, entry := range sr.Entries {
if seen[entry.DN] {
continue
}
seen[entry.DN] = true
uid, cn, mail := ldapIdentityFromEntry(entry, "")
if uid == "" {
continue
}
candidates = append(candidates, ldapEntryUser{uid: uid, cn: cn, mail: mail})
}
}
sr, err := conn.Search(ldap.NewSearchRequest(
searchBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
500, 0, false, baseFilter,
[]string{"uid", "cn", "mail"}, nil,
))
if err != nil {
resp.BadRequest(c, "LDAP 搜索失败: "+err.Error())
if len(candidates) == 0 {
resp.BadRequest(c, "LDAP 搜索无结果:请检查 Base DN/过滤器是否匹配目录结构")
return
}
@@ -556,11 +560,8 @@ func BatchImportLDAP(c *gin.Context) {
skipped := 0
var results []map[string]string
for _, entry := range sr.Entries {
uid := entry.GetAttributeValue("uid")
if uid == "" {
continue
}
for _, entry := range candidates {
uid := entry.uid
if len(req.UserIDs) > 0 {
found := false
for _, id := range req.UserIDs {
@@ -574,8 +575,8 @@ func BatchImportLDAP(c *gin.Context) {
}
}
cn := entry.GetAttributeValue("cn")
mail := entry.GetAttributeValue("mail")
cn := entry.cn
mail := entry.mail
var existing model.User
err := model.DB.Where("username = ?", uid).First(&existing).Error