package handler import ( "errors" "net/http" "github.com/gin-gonic/gin" "flowstack/internal/crypto" "flowstack/internal/database" "flowstack/internal/ldap" "flowstack/internal/model" ) // ---- LDAP 配置辅助 ---- type ldapRuntime struct { Host, BindDN, BindPassword, BaseDN, UserFilter, UsernameAttr, NameAttr, EmailAttr string Port int UseSSL bool Enabled, Configured bool } func loadLDAP() ldapRuntime { var s model.SystemSetting database.DB.First(&s, 1) rt := ldapRuntime{ Host: s.LdapHost, Port: s.LdapPort, UseSSL: s.LdapUseSSL, BindDN: s.LdapBindDN, BaseDN: s.LdapBaseDN, UserFilter: s.LdapUserFilter, UsernameAttr: s.LdapUsernameAttr, NameAttr: s.LdapNameAttr, EmailAttr: s.LdapEmailAttr, Enabled: s.LdapEnabled, } rt.Configured = rt.Host != "" && rt.BindDN != "" && rt.BaseDN != "" return rt } func (rt ldapRuntime) bindPassword() string { var s model.SystemSetting database.DB.First(&s, 1) if s.LdapBindPassword == "" { return "" } if crypto.IsEncrypted(s.LdapBindPassword) { if plain, err := crypto.DecryptField(s.LdapBindPassword); err == nil { return plain } } return s.LdapBindPassword } var errLDAPDisabled = errors.New("ldap disabled") // ldapVerifyUser 登录用:目录搜索 + 用户 DN 直连 bind func ldapVerifyUser(username, password string) (*ldap.Entry, error) { rt := loadLDAP() if !rt.Enabled || !rt.Configured { return nil, errLDAPDisabled } entry, err := ldap.SearchOne(rt.Host, rt.Port, rt.UseSSL, rt.BindDN, rt.bindPassword(), rt.BaseDN, rt.UserFilter, rt.UsernameAttr, username) if err != nil { return nil, err } if err := ldap.BindAsUser(rt.Host, rt.Port, rt.UseSSL, entry.DN, password); err != nil { return nil, err } return entry, nil } // syncLDAPUser LDAP 验证通过后同步/建号(JIT) func syncLDAPUser(entry *ldap.Entry) model.User { var user model.User if err := database.DB.Where("username = ?", entry.Username).First(&user).Error; err == nil { // 已存在:本地同名转 LDAP + 同步昵称邮箱 database.DB.Model(&user).Updates(map[string]any{ "auth_source": "ldap", "nickname": entry.DisplayName, "email": entry.Email, }) database.DB.First(&user, user.ID) return user } user = model.User{ Username: entry.Username, Nickname: entry.DisplayName, Email: entry.Email, Role: "user", AuthSource: "ldap", // 占位密码:本地不可登录 Password: mustHash("ldap-user-" + entry.Username + "-no-local-login"), } database.DB.Create(&user) return user } func mustHash(s string) string { h, _ := crypto.HashPassword(s) return h } // ---- 用户管理 ---- // ListUsers GET /api/users func ListUsers(c *gin.Context) { var list []model.User database.DB.Order("created_at ASC").Find(&list) c.JSON(http.StatusOK, list) } // CreateUser POST /api/users —— 创建本地用户 func CreateUser(c *gin.Context) { var body struct { Username string `json:"username" binding:"required"` Nickname string `json:"nickname"` Email string `json:"email"` Role string `json:"role"` Password string `json:"password"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "用户名不能为空"}) return } if body.Role != "admin" { body.Role = "user" } password := body.Password if password == "" { var err error password, err = crypto.RandomPassword(16) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } hash, err := crypto.HashPassword(password) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } user := model.User{ Username: body.Username, Nickname: body.Nickname, Email: body.Email, Role: body.Role, AuthSource: "local", Password: hash, } if err := database.DB.Create(&user).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "创建失败:用户名已存在"}) return } // 密码仅在创建时回显一次(指定密码时不回显明文,回显随机生成的) result := gin.H{"id": user.ID, "username": user.Username, "nickname": user.Nickname, "email": user.Email, "role": user.Role, "authSource": user.AuthSource} if body.Password == "" { result["initialPassword"] = password } c.JSON(http.StatusOK, result) } // ResetPassword POST /api/users/:id/reset-password —— LDAP 用户拒绝 func ResetPassword(c *gin.Context) { var user model.User if err := database.DB.First(&user, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) return } if user.AuthSource == "ldap" { c.JSON(http.StatusBadRequest, gin.H{"error": "LDAP 用户密码由目录服务管理"}) return } password, err := crypto.RandomPassword(16) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } hash, err := crypto.HashPassword(password) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } database.DB.Model(&user).Update("password", hash) c.JSON(http.StatusOK, gin.H{"success": true, "initialPassword": password}) } // DeleteUser DELETE /api/users/:id —— 不可删最后一个管理员/自己 func DeleteUser(c *gin.Context) { var user model.User if err := database.DB.First(&user, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) return } if user.Username == c.GetString("username") { c.JSON(http.StatusBadRequest, gin.H{"error": "不能删除当前登录用户"}) return } if user.Role == "admin" { var adminCount int64 database.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&adminCount) if adminCount <= 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "不能删除最后一个管理员"}) return } } database.DB.Delete(&user) c.JSON(http.StatusOK, gin.H{"success": true}) } // UpdateUser PUT /api/users/:id —— 角色/昵称/邮箱 func UpdateUser(c *gin.Context) { var user model.User if err := database.DB.First(&user, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) return } var body map[string]any _ = c.ShouldBindJSON(&body) if v, ok := body["role"].(string); ok && (v == "admin" || v == "user") { database.DB.Model(&user).Update("role", v) } if v, ok := body["nickname"].(string); ok { database.DB.Model(&user).Update("nickname", v) } if v, ok := body["email"].(string); ok { database.DB.Model(&user).Update("email", v) } database.DB.First(&user, user.ID) c.JSON(http.StatusOK, user) } // ---- LDAP 用户导入(LokiStack 交互与冲突策略) ---- // LdapListUsers POST /api/ldap/users —— 导入预览(未启用返回 400) func LdapListUsers(c *gin.Context) { rt := loadLDAP() if !rt.Enabled || !rt.Configured { c.JSON(http.StatusBadRequest, gin.H{"error": "LDAP 未启用或未配置"}) return } entries, err := ldap.SearchUsers(rt.Host, rt.Port, rt.UseSSL, rt.BindDN, rt.bindPassword(), rt.BaseDN, rt.UserFilter, rt.UsernameAttr, rt.NameAttr, rt.EmailAttr, 500) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, entries) } // LdapImport POST /api/users/ldap-import —— 批量导入(跳过已LDAP/本地同名转LDAP/新建) func LdapImport(c *gin.Context) { var body struct { Users []struct { Username string `json:"username"` Nickname string `json:"nickname"` Email string `json:"email"` } `json:"users" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil || len(body.Users) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要导入的用户"}) return } var created, converted, skipped, failed int var failedUsers []string for _, in := range body.Users { if in.Username == "" { continue } var exist model.User if err := database.DB.Where("username = ?", in.Username).First(&exist).Error; err == nil { if exist.AuthSource == "ldap" { skipped++ continue } // 本地同名 → 转为 LDAP database.DB.Model(&exist).Updates(map[string]any{"auth_source": "ldap", "nickname": in.Nickname, "email": in.Email}) converted++ continue } user := model.User{ Username: in.Username, Nickname: in.Nickname, Email: in.Email, Role: "user", AuthSource: "ldap", Password: mustHash("ldap-user-" + in.Username + "-no-local-login"), } if err := database.DB.Create(&user).Error; err != nil { failed++ failedUsers = append(failedUsers, in.Username) continue } created++ } c.JSON(http.StatusOK, gin.H{ "created": created, "converted": converted, "skipped": skipped, "failed": failed, "failedUsers": failedUsers, }) } // TestLDAP POST /api/settings/ldap/test —— 支持草稿直测 + 可选测试账号 func TestLDAP(c *gin.Context) { var s model.SystemSetting database.DB.First(&s, 1) var body struct { Enabled *bool `json:"enabled"` Host string `json:"host"` Port float64 `json:"port"` UseSSL *bool `json:"useSSL"` BindDN string `json:"bindDN"` BindPassword string `json:"bindPassword"` BaseDN string `json:"baseDN"` UserFilter string `json:"userFilter"` UsernameAttr string `json:"usernameAttr"` TestUsername string `json:"testUsername"` TestPassword string `json:"testPassword"` } _ = c.ShouldBindJSON(&body) host := body.Host if host == "" { host = s.LdapHost } bindDN := body.BindDN if bindDN == "" { bindDN = s.LdapBindDN } password := body.BindPassword if password == "" { password = "" var stored string database.DB.Select("ldap_bind_password").First(&s, 1) stored = s.LdapBindPassword if crypto.IsEncrypted(stored) { if plain, err := crypto.DecryptField(stored); err == nil { password = plain } } else { password = stored } } baseDN := body.BaseDN if baseDN == "" { baseDN = s.LdapBaseDN } port := int(body.Port) if port == 0 { port = s.LdapPort } if port == 0 { port = 389 } useSSL := s.LdapUseSSL if body.UseSSL != nil { useSSL = *body.UseSSL } usernameAttr := body.UsernameAttr if usernameAttr == "" { usernameAttr = s.LdapUsernameAttr } userFilter := body.UserFilter if userFilter == "" { userFilter = s.LdapUserFilter } if host == "" || bindDN == "" || baseDN == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请填写服务器/Bind DN/Base DN"}) return } conn, err := ldap.Dial(host, port, useSSL, bindDN, password) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()}) return } defer conn.Close() // 可选:测试账号级验证 if body.TestUsername != "" && body.TestPassword != "" { entry, err := ldap.SearchOne(host, port, useSSL, bindDN, password, baseDN, userFilter, usernameAttr, body.TestUsername) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "测试账号搜索失败: " + err.Error()}) return } if err := ldap.BindAsUser(host, port, useSSL, entry.DN, body.TestPassword); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "测试账号密码验证失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "连接与测试账号验证成功:" + entry.DisplayName + " (" + entry.Email + ")"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "LDAP 连接成功(管理员绑定通过)"}) }