package handlers import ( "encoding/json" "fmt" "net/http" "strings" "time" "ezops/internal/models" "github.com/gin-gonic/gin" "github.com/go-ldap/ldap/v3" "golang.org/x/crypto/bcrypt" ) // 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", "telephoneNumber", "title", "department"}, 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"` Phone string `json:"phone"` Title string `json:"title"` Department string `json:"department"` } 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"), Phone: e.GetAttributeValue("telephoneNumber"), Title: e.GetAttributeValue("title"), Department: e.GetAttributeValue("department"), }) } c.JSON(http.StatusOK, models.Response{Code: 200, Data: 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"` Username string `json:"username"` Nickname string `json:"nickname"` Email string `json:"email"` } `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 { uid := lu.UID if uid == "" { uid = lu.Username } cn := lu.CN if cn == "" { cn = lu.Nickname } mail := lu.Mail if mail == "" { mail = lu.Email } if uid == "" { skipped++ continue } // Check if already exists var existing models.User if err := h.db.Where("username = ?", uid).First(&existing).Error; err == nil { // Already exists, update info updates := map[string]interface{}{ "auth_source": "ldap", } if cn != "" { updates["real_name"] = cn } if mail != "" { updates["email"] = mail } h.db.Model(&existing).Updates(updates) imported++ continue } // Create new LDAP user user := models.User{ Username: uid, Password: "-", // LDAP users don't have local password RealName: cn, Email: 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}, }) } // CreateLDAPUser 在LDAP中创建用户 // POST /api/v1/admin/ldap-config/create-user func (h *LDAPHandler) CreateLDAPUser(c *gin.Context) { var req struct { Username string `json:"username"` Password string `json:"password"` DisplayName string `json:"display_name"` Email string `json:"email"` OUDN string `json:"ou_dn"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()}) return } if req.Username == "" || req.Password == "" { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "用户名和密码不能为空"}) return } var configs []models.LDAPConfig if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"}) return } cfg := configs[0] port := 389 if cfg.LDAPPort != nil { port = *cfg.LDAPPort } if req.OUDN == "" { req.OUDN = cfg.BaseDN } userDN := fmt.Sprintf("uid=%s,%s", ldap.EscapeFilter(req.Username), req.OUDN) // Parse user mapping mapping := map[string]string{"username": "uid", "email": "mail", "display_name": "cn"} if cfg.UserMapping != nil { _ = json.Unmarshal([]byte(*cfg.UserMapping), &mapping) } uidAttr := mapping["username"] if uidAttr == "" { uidAttr = "uid" } mailAttr := mapping["email"] if mailAttr == "" { mailAttr = "mail" } conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port)) if err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()}) return } defer conn.Close() conn.SetTimeout(5 * time.Second) if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()}) return } addReq := ldap.NewAddRequest(userDN, nil) addReq.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"}) addReq.Attribute(uidAttr, []string{req.Username}) addReq.Attribute("cn", []string{req.DisplayName}) addReq.Attribute("sn", []string{req.DisplayName}) if req.Email != "" { addReq.Attribute(mailAttr, []string{req.Email}) } addReq.Attribute("userPassword", []string{req.Password}) if err := conn.Add(addReq); err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建用户失败: " + err.Error()}) return } c.JSON(http.StatusOK, models.Response{Code: 200, Message: "用户创建成功", Data: gin.H{"dn": userDN}}) } // DeleteLDAPUser 删除LDAP条目(用户或OU) // POST /api/v1/admin/ldap-config/delete-user func (h *LDAPHandler) DeleteLDAPUser(c *gin.Context) { var req struct { DN string `json:"dn"` } if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"}) return } var configs []models.LDAPConfig if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"}) return } cfg := configs[0] port := 389 if cfg.LDAPPort != nil { port = *cfg.LDAPPort } conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port)) if err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()}) return } defer conn.Close() conn.SetTimeout(5 * time.Second) if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()}) return } delReq := ldap.NewDelRequest(req.DN, nil) if err := conn.Del(delReq); err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()}) return } c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"}) } // UpdateLDAPUser 修改 LDAP 用户属性 // POST /api/v1/admin/ldap-config/update-user func (h *LDAPHandler) UpdateLDAPUser(c *gin.Context) { var req struct { DN string `json:"dn"` CN string `json:"cn"` Mail string `json:"mail"` Phone string `json:"phone"` Title string `json:"title"` } if err := c.ShouldBindJSON(&req); err != nil || req.DN == "" { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "DN不能为空"}) return } var configs []models.LDAPConfig if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 { c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "LDAP未配置"}) return } cfg := configs[0] port := 389 if cfg.LDAPPort != nil { port = *cfg.LDAPPort } conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%d", cfg.LDAPHost, port)) if err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()}) return } defer conn.Close() conn.SetTimeout(5 * time.Second) if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "管理员认证失败: " + err.Error()}) return } // 先查当前值,做 diff sr, err := conn.Search(ldap.NewSearchRequest( req.DN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectClass=*)", []string{"cn", "mail", "telephoneNumber", "title"}, nil, )) if err != nil || len(sr.Entries) == 0 { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "用户不存在"}) return } entry := sr.Entries[0] modReq := ldap.NewModifyRequest(req.DN, nil) // cn oldCN := entry.GetAttributeValue("cn") if req.CN != "" && req.CN != oldCN { modReq.Replace("cn", []string{req.CN}) modReq.Replace("sn", []string{req.CN}) // sn 同步更新 } // mail oldMail := entry.GetAttributeValue("mail") if req.Mail != oldMail { if req.Mail == "" { oldVals := entry.GetAttributeValues("mail") if len(oldVals) > 0 { modReq.Delete("mail", oldVals) } } else { modReq.Replace("mail", []string{req.Mail}) } } // telephoneNumber oldPhone := entry.GetAttributeValue("telephoneNumber") if req.Phone != oldPhone { if req.Phone == "" { oldVals := entry.GetAttributeValues("telephoneNumber") if len(oldVals) > 0 { modReq.Delete("telephoneNumber", oldVals) } } else { modReq.Replace("telephoneNumber", []string{req.Phone}) } } // title oldTitle := entry.GetAttributeValue("title") if req.Title != oldTitle { if req.Title == "" { oldVals := entry.GetAttributeValues("title") if len(oldVals) > 0 { modReq.Delete("title", oldVals) } } else { modReq.Replace("title", []string{req.Title}) } } if len(modReq.Changes) > 0 { if err := conn.Modify(modReq); err != nil { c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "修改失败: " + err.Error()}) return } } c.JSON(http.StatusOK, models.Response{Code: 200, Message: "修改成功"}) }