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) }