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:
1 parent
2c96902fbd
commit
13fb3aceb3
6 files changed
+487
-266
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user