package handler import ( "encoding/json" "fmt" "net" "net/http" "strings" "time" "sql-platform/internal/model" "github.com/gin-gonic/gin" "github.com/go-ldap/ldap/v3" ) // GET /api/v1/ldap-config func GetLDAPConfig(c *gin.Context) { var configs []model.LDAPConfig model.DB.Order("id").Find(&configs) if len(configs) == 0 { c.JSON(http.StatusOK, nil) return } cfg := configs[0] // mask password in response cfg.AdminPassword = "******" // Parse user_mapping from JSON string to object for frontend type ldapResponse struct { model.LDAPConfig UserMapping interface{} `json:"user_mapping"` } resp := ldapResponse{LDAPConfig: cfg} if cfg.UserMapping != nil && *cfg.UserMapping != "" { var mapping map[string]string json.Unmarshal([]byte(*cfg.UserMapping), &mapping) resp.UserMapping = mapping } else { resp.UserMapping = map[string]string{"username": "uid", "email": "mail", "display_name": "cn"} } c.JSON(http.StatusOK, resp) } // PUT /api/v1/ldap-config func UpdateLDAPConfig(c *gin.Context) { var input map[string]interface{} if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } var configs []model.LDAPConfig model.DB.Order("id").Find(&configs) // Helper to get string from input 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 } // Serialize user_mapping to JSON string userMappingStr := "" if um, ok := input["user_mapping"]; ok { if bytes, err := json.Marshal(um); err == nil { userMappingStr = string(bytes) } } if len(configs) == 0 { // Create new config cfg := model.LDAPConfig{ LDAPHost: getStr("ldap_host"), AdminDN: getStr("admin_dn"), BaseDN: getStr("base_dn"), SearchFilter: func() *string { s := getStr("search_filter"); if s != "" { return &s }; return nil }(), UserMapping: func() *string { s := userMappingStr; if s != "" { return &s }; return nil }(), } if port := getInt("ldap_port"); port > 0 { cfg.LDAPPort = &port } if b := getBool("use_ldaps"); b != nil { cfg.UseLDAPS = b } if pw := getStr("admin_password"); pw != "" && pw != "******" { encPw, _ := encryptPassword(pw) cfg.AdminPassword = encPw } model.DB.Create(&cfg) cfg.AdminPassword = "******" c.JSON(http.StatusOK, cfg) } else { // Update existing 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 != "******" { encPw, _ := encryptPassword(v) updates["admin_password"] = encPw } 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 { model.DB.Model(&cfg).Updates(updates) } model.DB.First(&cfg, cfg.ID) cfg.AdminPassword = "******" c.JSON(http.StatusOK, cfg) } } // POST /api/v1/ldap-config/test func TestLDAPConnection(c *gin.Context) { var req struct { TestUser string `json:"test_user"` TestPassword string `json:"test_password"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } var cfg model.LDAPConfig if err := model.DB.Order("id").First(&cfg).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "LDAP 配置不存在,请先保存配置"}) return } if cfg.LDAPHost == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "LDAP 未配置"}) return } // Connect conn, err := dialLDAP(&cfg) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("连接失败: %s", err)}) return } defer conn.Close() // Admin bind (decrypt password) adminPw, _ := decryptPassword(cfg.AdminPassword) if err := conn.Bind(cfg.AdminDN, adminPw); 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 != "" && req.TestPassword != "__use_stored__" { userFilter := "" if cfg.SearchFilter != nil && *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)) } sr, err := conn.Search(ldap.NewSearchRequest( cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 10, false, userFilter, []string{"uid", "cn", "mail", "telephoneNumber", "title", "department", "memberOf", "distinguishedName"}, nil, )) if err != nil || len(sr.Entries) == 0 { c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("未找到用户: %s", req.TestUser)}) return } entry := sr.Entries[0] userDN := entry.DN // Verify password if err := conn.Bind(userDN, req.TestPassword); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "密码验证失败"}) return } // Extract user info userMapping := map[string]string{"username": "uid", "email": "mail", "display_name": "cn"} if cfg.UserMapping != nil && *cfg.UserMapping != "" { json.Unmarshal([]byte(*cfg.UserMapping), &userMapping) } userInfo := map[string]string{ "User lookup": "successful", "User ID": entry.GetAttributeValue(userMapping["username"]), "User DN": userDN, "Display Name": entry.GetAttributeValue(userMapping["display_name"]), "Email": entry.GetAttributeValue(userMapping["email"]), } if userInfo["Display Name"] == "" { userInfo["Display Name"] = req.TestUser } c.JSON(http.StatusOK, gin.H{ "success": true, "message": fmt.Sprintf("用户 %s 验证通过", req.TestUser), "user_info": userInfo, }) return } // Just test server connection c.JSON(http.StatusOK, gin.H{ "success": true, "message": fmt.Sprintf("LDAP 服务器 %s 连接成功", cfg.LDAPHost), }) } // POST /api/v1/ldap-config/ou-list func ListLDAPOUs(c *gin.Context) { var cfg model.LDAPConfig if err := model.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() adminPw, _ := decryptPassword(cfg.AdminPassword) if err := conn.Bind(cfg.AdminDN, adminPw); err != nil { c.JSON(http.StatusOK, []interface{}{}) return } sr, err := conn.Search(ldap.NewSearchRequest( cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 100, 0, false, "(objectClass=organizationalUnit)", []string{"ou", "dn", "description"}, nil, )) if err != nil { c.JSON(http.StatusOK, []interface{}{}) return } type OuItem struct { DN string `json:"dn"` Name string `json:"name"` } var items []OuItem for _, e := range sr.Entries { items = append(items, OuItem{ DN: e.DN, Name: e.GetAttributeValue("ou"), }) } c.JSON(http.StatusOK, items) } // POST /api/v1/ldap-config/user-list func ListLDAPUsers(c *gin.Context) { var req struct { OuDn string `json:"ou_dn"` } c.ShouldBindJSON(&req) var cfg model.LDAPConfig if err := model.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() adminPw2, _ := decryptPassword(cfg.AdminPassword) if err := conn.Bind(cfg.AdminDN, adminPw2); 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) } // POST /api/v1/ldap-config/create-ou func CreateLDAPOU(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "OU created (placeholder)"}) } // POST /api/v1/ldap-config/create-user func CreateLDAPUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "User created (placeholder)"}) } // POST /api/v1/ldap-config/batch-import-ldap func BatchImportLDAP(c *gin.Context) { 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, AuthSource: "ldap", 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 func DeleteLDAPUser(c *gin.Context) { var req struct { DN string `json:"dn" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 dn 参数"}) return } 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 } if err := conn.Del(ldap.NewDelRequest(req.DN, nil)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "删除用户失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "用户已从 LDAP 删除"}) } // 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" } 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: 3 * time.Second} return ldap.DialURL(uri, ldap.DialWithDialer(dialer)) }