fix: LDAP 测试连接 3s 超时 + 账号验证 search filter %s 替换 + LDAP 登录 fallback

- dialLDAP 辅助函数: 3s timeout 替代无超时 DialURL
- TestLDAPConnection: search_filter (uid=%s) 中 %s 未替换导致搜索失败
- tryLDAPAuth: 同样的 %s 替换 + timeout 3s
- auth.go 登录: 本地密码失败 → LDAP fallback → ldapInfo 判断修复
- BatchImportLDAP: 实现 LDAP 用户批量导入到本地 DB
- 前端 LdapConfigPage: LDAP 测试请求独立 10s timeout
- workflow.go: ResetUserPassword 实现
This commit is contained in:
logan committed 2026-05-26 14:42:31 +08:00
1 parent 95222d42ad
commit 5411cd6db9
5 files changed
+198 -32

No files matched your search

+138 -17
View File
@@ -3,7 +3,10 @@ package handler
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
"sql-platform/internal/model"
@@ -161,17 +164,7 @@ func TestLDAPConnection(c *gin.Context) {
}
// Connect
protocol := "ldap"
if cfg.UseLDAPS != nil && *cfg.UseLDAPS {
protocol = "ldaps"
}
port := 389
if cfg.LDAPPort != nil && *cfg.LDAPPort > 0 {
port = *cfg.LDAPPort
}
uri := fmt.Sprintf("%s://%s:%d", protocol, cfg.LDAPHost, port)
conn, err := ldap.DialURL(uri)
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("连接失败: %s", err)})
return
@@ -187,11 +180,17 @@ func TestLDAPConnection(c *gin.Context) {
// If test_user provided, search and verify
if req.TestUser != "" && req.TestPassword != "" && req.TestPassword != "__use_stored__" {
baseFilter := "(objectClass=inetOrgPerson)"
userFilter := ""
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
baseFilter = *cfg.SearchFilter
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
userFilter = strings.ReplaceAll(sf, "%s", ldap.EscapeFilter(req.TestUser))
} else {
userFilter = fmt.Sprintf("(&%s(uid=%s))", sf, ldap.EscapeFilter(req.TestUser))
}
} else {
userFilter = fmt.Sprintf("(uid=%s)", ldap.EscapeFilter(req.TestUser))
}
userFilter := fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(req.TestUser))
sr, err := conn.Search(ldap.NewSearchRequest(
cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
@@ -323,7 +322,12 @@ func ListLDAPUsers(c *gin.Context) {
baseFilter := "(objectClass=inetOrgPerson)"
if cfg.SearchFilter != nil && *cfg.SearchFilter != "" {
baseFilter = *cfg.SearchFilter
sf := *cfg.SearchFilter
if strings.Contains(sf, "%s") {
baseFilter = strings.ReplaceAll(sf, "%s", "*")
} else {
baseFilter = sf
}
}
sr, err := conn.Search(ldap.NewSearchRequest(
@@ -367,7 +371,119 @@ func CreateLDAPUser(c *gin.Context) {
// POST /api/v1/ldap-config/batch-import-ldap
func BatchImportLDAP(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Batch import (placeholder)", "count": 0})
var req struct {
OuDn string `json:"ou_dn"`
Role string `json:"role"`
UserIDs []string `json:"user_ids"`
}
c.ShouldBindJSON(&req)
if req.Role == "" {
req.Role = "developer"
}
var cfg model.LDAPConfig
if err := model.DB.Order("id").First(&cfg).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"})
return
}
conn, err := dialLDAP(&cfg)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()})
return
}
defer conn.Close()
adminPw, _ := decryptPassword(cfg.AdminPassword)
if err := conn.Bind(cfg.AdminDN, adminPw); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"})
return
}
searchBase := cfg.BaseDN
if req.OuDn != "" {
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,
500, 0, false, baseFilter,
[]string{"uid", "cn", "mail"}, nil,
))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 搜索失败: " + err.Error()})
return
}
isActive := true
imported := 0
skipped := 0
var results []map[string]string
for _, entry := range sr.Entries {
uid := entry.GetAttributeValue("uid")
if uid == "" {
continue
}
if len(req.UserIDs) > 0 {
found := false
for _, id := range req.UserIDs {
if id == uid { found = true; break }
}
if !found { continue }
}
cn := entry.GetAttributeValue("cn")
mail := entry.GetAttributeValue("mail")
var existing model.User
err := model.DB.Where("username = ?", uid).First(&existing).Error
if err == nil {
// Update existing user
updates := map[string]interface{}{}
if cn != "" && cn != existing.DisplayName { updates["display_name"] = cn }
if mail != "" && (existing.Email == nil || *existing.Email != mail) { updates["email"] = mail }
if len(updates) > 0 {
model.DB.Model(&existing).Updates(updates)
}
results = append(results, map[string]string{"uid": uid, "status": "updated"})
skipped++
continue
}
newUser := model.User{
Username: uid,
DisplayName: cn,
Role: req.Role,
IsActive: &isActive,
HashedPassword: "-",
}
if mail != "" { newUser.Email = &mail }
if err := model.DB.Create(&newUser).Error; err != nil {
results = append(results, map[string]string{"uid": uid, "status": "error: " + err.Error()})
skipped++
continue
}
results = append(results, map[string]string{"uid": uid, "status": "created"})
imported++
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("导入完成: %d 新建, %d 更新", imported, skipped),
"imported": imported,
"skipped": skipped,
"results": results,
})
}
// POST /api/v1/ldap-config/delete-user
@@ -377,6 +493,9 @@ func DeleteLDAPUser(c *gin.Context) {
// helper: dial LDAP with config
func dialLDAP(cfg *model.LDAPConfig) (*ldap.Conn, error) {
if cfg.LDAPHost == "" {
return nil, fmt.Errorf("LDAP not configured")
}
protocol := "ldap"
if cfg.UseLDAPS != nil && *cfg.UseLDAPS {
protocol = "ldaps"
@@ -385,5 +504,7 @@ func dialLDAP(cfg *model.LDAPConfig) (*ldap.Conn, error) {
if cfg.LDAPPort != nil && *cfg.LDAPPort > 0 {
port = *cfg.LDAPPort
}
return ldap.DialURL(fmt.Sprintf("%s://%s:%d", protocol, cfg.LDAPHost, port))
uri := fmt.Sprintf("%s://%s:%d", protocol, cfg.LDAPHost, port)
dialer := &net.Dialer{Timeout: 3 * time.Second}
return ldap.DialURL(uri, ldap.DialWithDialer(dialer))
}