Files
aiops/backend/internal/api/handlers/ldap.go
T

704 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, models.Response{Code: 200, Data: 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, models.Response{Code: 200, Data: gin.H{"success": true, "message": fmt.Sprintf("认证成功!用户DN: %s", userDN)}})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: 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, 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},
})
}
// ListLDAPOUs 列出 LDAP 组织单元
// POST /api/v1/admin/ldap-config/ou-list
func (h *LDAPHandler) ListLDAPOUs(c *gin.Context) {
var configs []models.LDAPConfig
if err := h.db.Order("id").Find(&configs).Error; err != nil || len(configs) == 0 {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
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.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
defer conn.Close()
conn.SetTimeout(5 * time.Second)
if err := conn.Bind(cfg.AdminDN, cfg.AdminPassword); err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
searchReq := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=organizationalUnit)",
[]string{"ou", "description"},
nil,
)
sr, err := conn.Search(searchReq)
if err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Data: []interface{}{}})
return
}
type OU struct {
DN string `json:"dn"`
Name string `json:"name"`
Description string `json:"description"`
}
var ous []OU
for _, e := range sr.Entries {
ous = append(ous, OU{
DN: e.DN,
Name: e.GetAttributeValue("ou"),
Description: e.GetAttributeValue("description"),
})
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: ous})
}
// CreateLDAPOU 创建组织单元
// POST /api/v1/admin/ldap-config/create-ou
func (h *LDAPHandler) CreateLDAPOU(c *gin.Context) {
var req struct {
OUName string `json:"ou_name"`
ParentDN string `json:"parent_dn"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
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.ParentDN == "" {
req.ParentDN = cfg.BaseDN
}
ouDN := fmt.Sprintf("ou=%s,%s", ldap.EscapeFilter(req.OUName), req.ParentDN)
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(ouDN, nil)
addReq.Attribute("objectClass", []string{"organizationalUnit", "top"})
addReq.Attribute("ou", []string{req.OUName})
if req.Description != "" {
addReq.Attribute("description", []string{req.Description})
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建OU失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "OU创建成功", Data: gin.H{"dn": ouDN}})
}
// 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: "删除成功"})
}