From c51f619a7dab2b94351c7412e0b0539d8cb5dcd5 Mon Sep 17 00:00:00 2001 From: logan Date: Sat, 30 May 2026 10:24:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0LDAP=E5=88=9B?= =?UTF-8?q?=E5=BB=BAOU=E5=92=8C=E7=94=A8=E6=88=B7(LDAP=20Add)+=E5=BC=B9?= =?UTF-8?q?=E6=A1=86=E9=A3=8E=E6=A0=BC=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/ldap.go | 117 +++++++++++++++++++++++++- frontend/src/pages/LdapConfigPage.tsx | 99 +++++++++++----------- 2 files changed, 167 insertions(+), 49 deletions(-) diff --git a/backend/internal/handler/ldap.go b/backend/internal/handler/ldap.go index 1ec77fd..0e4aa46 100644 --- a/backend/internal/handler/ldap.go +++ b/backend/internal/handler/ldap.go @@ -361,12 +361,125 @@ func ListLDAPUsers(c *gin.Context) { // POST /api/v1/ldap-config/create-ou func CreateLDAPOU(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "OU created (placeholder)"}) + var req struct { + OuName string `json:"ou_name"` + ParentDN string `json:"parent_dn"` + Description string `json:"description"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.OuName == "" { + c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 ou_name 参数"}) + 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 + } + + ouDN := fmt.Sprintf("ou=%s,%s", ldap.EscapeFilter(req.OuName), req.ParentDN) + 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.StatusBadRequest, gin.H{"detail": "创建 OU 失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("OU %s 创建成功", req.OuName), "dn": ouDN}) } // POST /api/v1/ldap-config/create-user func CreateLDAPUser(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "User created (placeholder)"}) + var req struct { + Username string `json:"username"` + DisplayName string `json:"display_name"` + Email string `json:"email"` + Password string `json:"password"` + OuDn string `json:"ou_dn"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" { + c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 username 或 password 参数"}) + 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 + } + + // Parse user_mapping for attribute names + userMapping := map[string]string{"username": "uid", "email": "mail", "display_name": "cn"} + if cfg.UserMapping != nil && *cfg.UserMapping != "" { + json.Unmarshal([]byte(*cfg.UserMapping), &userMapping) + } + + 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 + } + + uidAttr := userMapping["username"] + if uidAttr == "" { + uidAttr = "uid" + } + cnAttr := userMapping["display_name"] + if cnAttr == "" { + cnAttr = "cn" + } + mailAttr := userMapping["email"] + if mailAttr == "" { + mailAttr = "mail" + } + + userDN := fmt.Sprintf("%s=%s,%s", uidAttr, ldap.EscapeFilter(req.Username), req.OuDn) + displayName := req.DisplayName + if displayName == "" { + displayName = req.Username + } + + addReq := ldap.NewAddRequest(userDN, nil) + addReq.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"}) + addReq.Attribute(uidAttr, []string{req.Username}) + addReq.Attribute(cnAttr, []string{displayName}) + addReq.Attribute("sn", []string{displayName}) + addReq.Attribute("userPassword", []string{req.Password}) + if req.Email != "" { + addReq.Attribute(mailAttr, []string{req.Email}) + } + + if err := conn.Add(addReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": "创建用户失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("用户 %s 创建成功", req.Username), "dn": userDN}) } // POST /api/v1/ldap-config/batch-import-ldap diff --git a/frontend/src/pages/LdapConfigPage.tsx b/frontend/src/pages/LdapConfigPage.tsx index fb0d240..3cd5590 100644 --- a/frontend/src/pages/LdapConfigPage.tsx +++ b/frontend/src/pages/LdapConfigPage.tsx @@ -6,7 +6,7 @@ import { toast } from '@/hooks/useToast'; import { Save, ChevronRight, ChevronDown, RefreshCw, CheckCircle, XCircle, Trash2, KeyRound, FolderOpen, Users, - Shield, AlertTriangle, Import, + Shield, AlertTriangle, Import, UserPlus, } from 'lucide-react'; /* ── types ── */ @@ -613,19 +613,22 @@ export default function LdapConfigPage() { {/* ── 创建 OU 弹框 ── */} {showCreateOu && ( - setShowCreateOu(false)} width="w-[460px]"> -
-
-

新建组织单元 (OU)

- + setShowCreateOu(false)} width="max-w-md"> +
+
+
+ +

新建组织单元 (OU)

+
+
-
+
setNewOuName(e.target.value)} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400" + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" placeholder="如: devops, engineering" />
@@ -634,7 +637,7 @@ export default function LdapConfigPage() { setNewOuDesc(e.target.value)} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400" + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" placeholder="可选,如: 研发团队" />
-
- +
+ @@ -666,41 +669,42 @@ export default function LdapConfigPage() { {/* ── 添加用户弹框 ── */} {showAddUser && ( - setShowAddUser(false)} width="w-[500px]"> -
-
-

添加用户到 LDAP

- -
-
-
-
- - setAddUsername(e.target.value)} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400" - placeholder="登录名,如 zhangsan" - /> -
-
- - setAddPassword(e.target.value)} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400" - placeholder="初始密码" - /> -
+ setShowAddUser(false)} width="max-w-md"> +
+
+
+ +

添加用户到 LDAP

-
+ +
+
+
+ + setAddUsername(e.target.value)} + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" + placeholder="登录名,如 zhangsan" + /> +
+
+ + setAddPassword(e.target.value)} + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" + placeholder="初始密码" + /> +
+
setAddDisplayName(e.target.value)} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400" + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" placeholder="如: 张三" />
@@ -709,7 +713,7 @@ export default function LdapConfigPage() { setAddEmail(e.target.value)} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400" + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" placeholder="可选" />
@@ -719,20 +723,21 @@ export default function LdapConfigPage() {
-
- +
+