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

+10 -41
View File
@@ -227,35 +227,11 @@ func tryLDAPAuth(username, password string) (*ldap.Conn, *LDAPAuthInfo, error) {
return nil, nil, fmt.Errorf("LDAP admin bind failed: %w", err)
}
// 4. Build search filter
userFilter := ""
baseFilter := "(objectClass=inetOrgPerson)"
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
userFilter = strings.ReplaceAll(sf, "%s", ldap.EscapeFilter(username))
} else {
baseFilter = sf
userFilter = fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(username))
}
} else {
userFilter = fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(username))
// 4-5. 多轮过滤器搜索用户(OpenLDAP + AD 兜底,借鉴 aiops)
entry, err := ldapSearchFirstUser(conn, &cfg, username)
if err != nil {
return nil, nil, err
}
// 5. Search user
searchReq := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
1, 10, false,
userFilter,
[]string{"uid", "cn", "mail", "telephoneNumber"},
nil,
)
sr, err := conn.Search(searchReq)
if err != nil || len(sr.Entries) == 0 {
return nil, nil, fmt.Errorf("user not found in LDAP")
}
entry := sr.Entries[0]
userDN := entry.DN
// 6. Verify password by binding as user
@@ -273,22 +249,15 @@ func tryLDAPAuth(username, password string) (*ldap.Conn, *LDAPAuthInfo, error) {
json.Unmarshal([]byte(*cfg.UserMapping), &userMapping)
}
getAttr := func(key string) string {
if v := entry.GetAttributeValue(key); v != "" {
return v
}
return ""
}
// 7. 属性回退链提取身份(OpenLDAP 与 AD 字段差异由 ldapEntryAttr 抹平)
identUser, identName, identEmail := ldapIdentityFromEntry(entry, username)
info := &LDAPAuthInfo{
Username: getAttr(userMapping["username"]),
DisplayName: getAttr(userMapping["display_name"]),
Username: identUser,
DisplayName: identName,
}
if email := getAttr(userMapping["email"]); email != "" {
info.Email = &email
}
if info.Username == "" {
info.Username = username
if identEmail != "" {
info.Email = &identEmail
}
return nil, info, nil // conn already closed by defer