package handlers import ( "encoding/json" "fmt" "net" "net/http" "strings" "time" "ezops/internal/models" "github.com/gin-gonic/gin" "github.com/go-ldap/ldap/v3" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // LDAPHandler LDAP处理器 type LDAPHandler struct { db *gorm.DB } // NewLDAPHandler 创建LDAP处理器 func NewLDAPHandler(db *gorm.DB) *LDAPHandler { return &LDAPHandler{db: db} } // LDAPAuthInfo LDAP认证返回的用户信息 type LDAPAuthInfo struct { Username string DisplayName string Email string } // dialLDAP 连接LDAP服务器 func dialLDAP(cfg *models.LDAPConfig) (*ldap.Conn, error) { if cfg.LDAPHost == "" { return nil, fmt.Errorf("LDAP not configured") } 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) dialer := &net.Dialer{Timeout: 5 * time.Second} return ldap.DialURL(uri, ldap.DialWithDialer(dialer)) } // tryLDAPAuth 尝试LDAP认证,返回用户信息 func tryLDAPAuth(db *gorm.DB, username, password string) (*LDAPAuthInfo, error) { var cfg models.LDAPConfig if err := db.Order("id").First(&cfg).Error; err != nil { return nil, fmt.Errorf("LDAP未配置") } if cfg.LDAPHost == "" { return nil, fmt.Errorf("LDAP not configured") } conn, err := dialLDAP(&cfg) if err != nil { return nil, fmt.Errorf("LDAP连接失败: %w", err) } defer conn.Close() // Admin bind if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { return nil, fmt.Errorf("LDAP管理员绑定失败: %w", err) } // 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)) } // 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, fmt.Errorf("LDAP用户不存在") } entry := sr.Entries[0] userDN := entry.DN // Verify password by binding as user if err := conn.Bind(userDN, password); err != nil { return nil, fmt.Errorf("LDAP密码验证失败: %w", err) } // Extract user info using mapping userMapping := map[string]string{ "username": "uid", "email": "mail", "display_name": "cn", } if cfg.UserMapping != nil && *cfg.UserMapping != "" { json.Unmarshal([]byte(*cfg.UserMapping), &userMapping) } getAttr := func(key string) string { if v := entry.GetAttributeValue(key); v != "" { return v } return "" } info := &LDAPAuthInfo{ Username: getAttr(userMapping["username"]), DisplayName: getAttr(userMapping["display_name"]), Email: getAttr(userMapping["email"]), } if info.Username == "" { info.Username = username } return info, nil } // GetLDAPConfig 获取LDAP配置 // GET /api/v1/admin/ldap-config func (h *LDAPHandler) GetLDAPConfig(c *gin.Context) { var configs []models.LDAPConfig h.db.Order("id").Find(&configs) if len(configs) == 0 { c.JSON(http.StatusOK, gin.H{}) return } cfg := configs[0] // mask password cfg.AdminPassword = "******" c.JSON(http.StatusOK, cfg) } // UpdateLDAPConfig 更新LDAP配置 // PUT /api/v1/admin/ldap-config func (h *LDAPHandler) UpdateLDAPConfig(c *gin.Context) { var input map[string]interface{} if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()}) return } var configs []models.LDAPConfig h.db.Order("id").Find(&configs) getStr := func(key string) string { if v, ok := input[key].(string); ok { return v } return "" } getInt := func(key string) int { if v, ok := input[key].(float64); ok { return int(v) } return 0 } getBool := func(key string) *bool { if v, ok := input[key].(bool); ok { return &v } return nil } userMappingStr := "" if um, ok := input["user_mapping"]; ok { if bytes, err := json.Marshal(um); err == nil { userMappingStr = string(bytes) } } if len(configs) == 0 { // Create cfg := models.LDAPConfig{ LDAPHost: getStr("ldap_host"), AdminDN: getStr("admin_dn"), BaseDN: getStr("base_dn"), AdminPassword: getStr("admin_password"), } if sf := getStr("search_filter"); sf != "" { cfg.SearchFilter = &sf } if userMappingStr != "" { cfg.UserMapping = &userMappingStr } if port := getInt("ldap_port"); port > 0 { cfg.LDAPPort = &port } if b := getBool("use_ldaps"); b != nil { cfg.UseLDAPS = b } h.db.Create(&cfg) cfg.AdminPassword = "******" c.JSON(http.StatusOK, models.Response{Code: 200, Message: "LDAP配置已创建", Data: cfg}) } else { // Update cfg := configs[0] updates := map[string]interface{}{} if v := getStr("ldap_host"); v != "" { updates["ldap_host"] = v } if v := getStr("base_dn"); v != "" { updates["base_dn"] = v } if v := getStr("admin_dn"); v != "" { updates["admin_dn"] = v } if v := getStr("search_filter"); v != "" { updates["search_filter"] = v } if userMappingStr != "" { updates["user_mapping"] = userMappingStr } if v := getStr("admin_password"); v != "" && v != "******" { updates["admin_password"] = v } if port := getInt("ldap_port"); port > 0 { updates["ldap_port"] = port } if b := getBool("use_ldaps"); b != nil { updates["use_ldaps"] = *b } if len(updates) > 0 { h.db.Model(&cfg).Updates(updates) } h.db.First(&cfg, cfg.ID) cfg.AdminPassword = "******" c.JSON(http.StatusOK, models.Response{Code: 200, Message: "LDAP配置已更新", Data: cfg}) } } // TestLDAPConnection 测试LDAP连接 // POST /api/v1/admin/ldap-config/test func (h *LDAPHandler) TestLDAPConnection(c *gin.Context) { var req struct { TestUser string `json:"test_user"` TestPassword string `json:"test_password"` } c.ShouldBindJSON(&req) var cfg models.LDAPConfig if err := h.db.Order("id").First(&cfg).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "LDAP配置不存在,请先保存配置"}) return } if cfg.LDAPHost == "" { c.JSON(http.StatusOK, gin.H{"success": false, "message": "LDAP未配置"}) return } conn, err := dialLDAP(&cfg) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("连接失败: %s", err)}) return } defer conn.Close() if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("管理员绑定失败: %s", err)}) return } // If test_user provided, search and verify if req.TestUser != "" && req.TestPassword != "" { 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(req.TestUser)) } else { baseFilter = sf userFilter = fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(req.TestUser)) } } else { userFilter = fmt.Sprintf("(&%s(uid=%s))", baseFilter, ldap.EscapeFilter(req.TestUser)) } sr, err := conn.Search(ldap.NewSearchRequest( cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 10, false, userFilter, []string{"uid", "cn", "mail"}, nil, )) if err != nil || len(sr.Entries) == 0 { c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("用户 %s 不存在", req.TestUser)}) return } userDN := sr.Entries[0].DN if err := conn.Bind(userDN, req.TestPassword); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("密码验证失败: %s", err)}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": fmt.Sprintf("认证成功!用户DN: %s", userDN)}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "LDAP连接成功,管理员认证通过"}) } // ListLDAPUsers 列出LDAP用户 // POST /api/v1/admin/ldap-config/users func (h *LDAPHandler) ListLDAPUsers(c *gin.Context) { var req struct { OuDn string `json:"ou_dn"` } c.ShouldBindJSON(&req) var cfg models.LDAPConfig if err := h.db.Order("id").First(&cfg).Error; err != nil { c.JSON(http.StatusOK, []interface{}{}) return } conn, err := dialLDAP(&cfg) if err != nil { c.JSON(http.StatusOK, []interface{}{}) return } defer conn.Close() if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusOK, []interface{}{}) 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, 200, 0, false, baseFilter, []string{"uid", "cn", "mail", "dn"}, nil, )) if err != nil { c.JSON(http.StatusOK, []interface{}{}) return } type LdapUser struct { DN string `json:"dn"` UID string `json:"uid"` CN string `json:"cn"` Mail string `json:"mail"` } 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"), }) } c.JSON(http.StatusOK, users) } // BatchImportLDAP 批量导入LDAP用户 // POST /api/v1/admin/ldap-config/import func (h *LDAPHandler) BatchImportLDAP(c *gin.Context) { var req struct { Users []struct { UID string `json:"uid"` CN string `json:"cn"` Mail string `json:"mail"` } `json:"users"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()}) return } imported := 0 skipped := 0 for _, lu := range req.Users { if lu.UID == "" { skipped++ continue } // Check if already exists var existing models.User if err := h.db.Where("username = ?", lu.UID).First(&existing).Error; err == nil { // Already exists, update info updates := map[string]interface{}{ "auth_source": "ldap", "is_active": true, } if lu.CN != "" { updates["real_name"] = lu.CN } if lu.Mail != "" { updates["email"] = lu.Mail } h.db.Model(&existing).Updates(updates) imported++ continue } // Create new LDAP user user := models.User{ Username: lu.UID, Password: "-", // LDAP users don't have local password RealName: lu.CN, Email: lu.Mail, Status: 1, AuthSource: "ldap", } if err := h.db.Create(&user).Error; err != nil { skipped++ continue } imported++ } // bcrypt import to avoid unused import error _ = bcrypt.MinCost c.JSON(http.StatusOK, models.Response{ Code: 200, Message: fmt.Sprintf("导入完成: %d 个用户成功, %d 个跳过", imported, skipped), Data: gin.H{"imported": imported, "skipped": skipped}, }) }