feat(ldap): Active Directory 双目录支持与 Docker 构建优化
- 新增 ldap_ad.go,支持 AD 密码编码、字段映射与目录类型判断 - 重构 ldap/ldap_users/ldap_ou,兼容 OpenLDAP 与 AD 登录、导入、OU 浏览 - 前端系统设置增加目录类型选择与 AD 配置项 - Dockerfile 按 TARGETARCH 单架构编译 Agent,限制 Node 内存防 OOM - run.sh 新增 docker fast/upgrade,README 补充 LDAP/AD 与常见问题
This commit is contained in:
1 parent
0f0f2770e1
commit
78c27037e3
12 files changed
+613
-226
No files matched your search
@@ -0,0 +1,154 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
ldapServerOpenLDAP = "openldap"
|
||||
ldapServerAD = "ad"
|
||||
)
|
||||
|
||||
// ldapIsAD 判断当前配置是否为 Active Directory // Logan.Li
|
||||
func ldapIsAD(cfg *model.LDAPConfig) bool {
|
||||
if cfg == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(cfg.ServerType), ldapServerAD)
|
||||
}
|
||||
|
||||
// ldapDefaultUserMapping 按目录类型返回默认字段映射 // Logan.Li
|
||||
func ldapDefaultUserMapping(cfg *model.LDAPConfig) map[string]string {
|
||||
if ldapIsAD(cfg) {
|
||||
return map[string]string{
|
||||
"username": "sAMAccountName",
|
||||
"email": "mail",
|
||||
"display_name": "displayName",
|
||||
}
|
||||
}
|
||||
return map[string]string{
|
||||
"username": "uid",
|
||||
"email": "mail",
|
||||
"display_name": "cn",
|
||||
}
|
||||
}
|
||||
|
||||
// ldapEncodeADPassword 将明文密码编码为 AD unicodePwd(UTF-16LE 带引号) // Logan.Li
|
||||
func ldapEncodeADPassword(password string) []byte {
|
||||
quoted := `"` + password + `"`
|
||||
runes := utf16.Encode([]rune(quoted))
|
||||
buf := make([]byte, len(runes)*2)
|
||||
for i, r := range runes {
|
||||
buf[i*2] = byte(r)
|
||||
buf[i*2+1] = byte(r >> 8)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// ldapBuildUserDN 按目录类型构造用户 DN // Logan.Li
|
||||
func ldapBuildUserDN(cfg *model.LDAPConfig, username, displayName, parentDN string) (string, error) {
|
||||
parentDN = strings.TrimSpace(parentDN)
|
||||
if parentDN == "" {
|
||||
return "", fmt.Errorf("父级 DN 不能为空")
|
||||
}
|
||||
if ldapIsAD(cfg) {
|
||||
cn := strings.TrimSpace(displayName)
|
||||
if cn == "" {
|
||||
cn = username
|
||||
}
|
||||
return fmt.Sprintf("CN=%s,%s", ldap.EscapeDN(cn), parentDN), nil
|
||||
}
|
||||
if strings.TrimSpace(username) == "" {
|
||||
return "", fmt.Errorf("用户名不能为空")
|
||||
}
|
||||
return fmt.Sprintf("uid=%s,%s", ldap.EscapeFilter(username), parentDN), nil
|
||||
}
|
||||
|
||||
// ldapOUListFilters 返回 OU/容器列表过滤器,兼容 OpenLDAP 与 AD // Logan.Li
|
||||
func ldapOUListFilters(cfg *model.LDAPConfig) []string {
|
||||
if ldapIsAD(cfg) {
|
||||
return []string{
|
||||
"(|(objectClass=organizationalUnit)(objectClass=container))",
|
||||
}
|
||||
}
|
||||
return []string{"(objectClass=organizationalUnit)"}
|
||||
}
|
||||
|
||||
// ldapOUEntryName 读取 OU/容器显示名 // Logan.Li
|
||||
func ldapOUEntryName(entry *ldap.Entry) string {
|
||||
if v := entry.GetAttributeValue("ou"); v != "" {
|
||||
return v
|
||||
}
|
||||
return ldapEntryAttr(entry, "cn", "name")
|
||||
}
|
||||
|
||||
// ldapAddOpenLDAPUser 在 OpenLDAP 中创建 inetOrgPerson 用户 // Logan.Li
|
||||
func ldapAddOpenLDAPUser(conn *ldap.Conn, cfg *model.LDAPConfig, userDN, username, displayName, email, password string) error {
|
||||
mapping := ldapUserMapping(cfg)
|
||||
uidAttr := mapping["username"]
|
||||
if uidAttr == "" {
|
||||
uidAttr = "uid"
|
||||
}
|
||||
mailAttr := mapping["email"]
|
||||
if mailAttr == "" {
|
||||
mailAttr = "mail"
|
||||
}
|
||||
cn := displayName
|
||||
if cn == "" {
|
||||
cn = username
|
||||
}
|
||||
|
||||
addReq := ldap.NewAddRequest(userDN, nil)
|
||||
addReq.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
|
||||
addReq.Attribute(uidAttr, []string{username})
|
||||
addReq.Attribute("cn", []string{cn})
|
||||
addReq.Attribute("sn", []string{cn})
|
||||
if email != "" {
|
||||
addReq.Attribute(mailAttr, []string{email})
|
||||
}
|
||||
addReq.Attribute("userPassword", []string{password})
|
||||
return conn.Add(addReq)
|
||||
}
|
||||
|
||||
// ldapAddADUser 在 Active Directory 中创建并启用用户账号 // Logan.Li
|
||||
func ldapAddADUser(conn *ldap.Conn, userDN, username, displayName, email, password string) error {
|
||||
cn := strings.TrimSpace(displayName)
|
||||
if cn == "" {
|
||||
cn = username
|
||||
}
|
||||
sn := cn
|
||||
if idx := strings.LastIndex(cn, " "); idx > 0 {
|
||||
sn = cn[idx+1:]
|
||||
}
|
||||
|
||||
addReq := ldap.NewAddRequest(userDN, nil)
|
||||
addReq.Attribute("objectClass", []string{"top", "person", "organizationalPerson", "user"})
|
||||
addReq.Attribute("cn", []string{cn})
|
||||
addReq.Attribute("sAMAccountName", []string{username})
|
||||
addReq.Attribute("sn", []string{sn})
|
||||
if email != "" {
|
||||
addReq.Attribute("mail", []string{email})
|
||||
}
|
||||
// 先以禁用状态创建,设置密码后再启用 // Logan.Li
|
||||
addReq.Attribute("userAccountControl", []string{"514"})
|
||||
if err := conn.Add(addReq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
modPwd := ldap.NewModifyRequest(userDN, nil)
|
||||
modPwd.Replace("unicodePwd", []string{string(ldapEncodeADPassword(password))})
|
||||
if err := conn.Modify(modPwd); err != nil {
|
||||
_ = conn.Del(ldap.NewDelRequest(userDN, nil))
|
||||
return fmt.Errorf("设置 AD 密码失败(需 LDAPS 连接): %w", err)
|
||||
}
|
||||
|
||||
modEnable := ldap.NewModifyRequest(userDN, nil)
|
||||
modEnable.Replace("userAccountControl", []string{"512"})
|
||||
return conn.Modify(modEnable)
|
||||
}
|
||||
Reference in new issue
Block a user