package controller import ( "fmt" "net/http" "ezops/model" "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 model.LDAPConfig if err := h.db.Order("id").First(&cfg).Error; err != nil { c.JSON(http.StatusOK, model.Response{Code: 200, Data: []interface{}{}}) return } conn, err := dialLDAP(&cfg) if err != nil { c.JSON(http.StatusOK, model.Response{Code: 200, Data: []interface{}{}}) return } defer conn.Close() if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusOK, model.Response{Code: 200, Data: []interface{}{}}) return } searchBase := cfg.BaseDN if req.OuDn != "" { searchBase = req.OuDn } type LdapUser struct { DN string `json:"dn"` UID string `json:"uid"` Username string `json:"username"` CN string `json:"cn"` Mail string `json:"mail"` Phone string `json:"phone"` Title string `json:"title"` Department string `json:"department"` } var users []LdapUser seenDNs := map[string]bool{} for _, filter := range ldapUserListFilters(&cfg) { sr, err := ldapSearchUsers(conn, searchBase, filter, 500) if err != nil { continue } for _, e := range sr.Entries { if seenDNs[e.DN] { continue } info := ldapIdentityFromEntry(&cfg, e, "") if info.Username == "" { continue } seenDNs[e.DN] = true users = append(users, LdapUser{ DN: e.DN, UID: info.Username, Username: info.Username, CN: info.DisplayName, Mail: info.Email, Phone: ldapEntryAttr(e, "telephoneNumber", "mobile"), Title: ldapEntryAttr(e, "title"), Department: ldapEntryAttr(e, "department"), }) } } c.JSON(http.StatusOK, model.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, model.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 model.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 := model.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, model.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, model.Response{Code: 400, Message: err.Error()}) return } if req.Username == "" || req.Password == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "用户名和密码不能为空"}) return } var configs []model.LDAPConfig if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LDAP未配置"}) return } cfg := configs[0] if req.OUDN == "" { req.OUDN = cfg.BaseDN } userDN, err := ldapBuildUserDN(&cfg, req.Username, req.DisplayName, req.OUDN) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } if ldapIsAD(&cfg) && (cfg.UseLDAPS == nil || !*cfg.UseLDAPS) { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "Active Directory 创建用户需启用 LDAPS"}) return } conn, err := dialLDAP(&cfg) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()}) return } defer conn.Close() if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "管理员认证失败: " + err.Error()}) return } if ldapIsAD(&cfg) { if err := ldapAddADUser(conn, userDN, req.Username, req.DisplayName, req.Email, req.Password); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建 AD 用户失败: " + err.Error()}) return } } else { if err := ldapAddOpenLDAPUser(conn, &cfg, userDN, req.Username, req.DisplayName, req.Email, req.Password); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建用户失败: " + err.Error()}) return } } c.JSON(http.StatusOK, model.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, model.Response{Code: 400, Message: "DN不能为空"}) return } var configs []model.LDAPConfig if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LDAP未配置"}) return } cfg := configs[0] conn, err := dialLDAP(&cfg) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()}) return } defer conn.Close() if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "管理员认证失败: " + err.Error()}) return } delReq := ldap.NewDelRequest(req.DN, nil) if err := conn.Del(delReq); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败: " + err.Error()}) return } c.JSON(http.StatusOK, model.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, model.Response{Code: 400, Message: "DN不能为空"}) return } var configs []model.LDAPConfig if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "LDAP未配置"}) return } cfg := configs[0] conn, err := dialLDAP(&cfg) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "LDAP连接失败: " + err.Error()}) return } defer conn.Close() if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "管理员认证失败: " + err.Error()}) return } // 先查当前值,做 diff(AD 同时读取 displayName) // Logan.Li sr, err := conn.Search(ldap.NewSearchRequest( req.DN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectClass=*)", []string{"cn", "displayName", "mail", "telephoneNumber", "title"}, nil, )) if err != nil || len(sr.Entries) == 0 { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "用户不存在"}) return } entry := sr.Entries[0] modReq := ldap.NewModifyRequest(req.DN, nil) // cn / displayName(AD 优先 displayName) // Logan.Li oldCN := ldapEntryAttr(entry, "displayName", "cn") if req.CN != "" && req.CN != oldCN { modReq.Replace("cn", []string{req.CN}) if ldapIsAD(&cfg) { modReq.Replace("displayName", []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, model.Response{Code: 500, Message: "修改失败: " + err.Error()}) return } } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "修改成功"}) }