Files
idsync/internal/ldaplib/read.go
T
caowangrenbo 979a98d502 refactor: config + lldaplib 收敛到 LLDAP 形态(读只读、删 OpenLDAP 写路径与密码包)
- config 重写:删 user-ou/inactive-ou/group-root-* / group-object-class /
  group-member-attr / initial-password;新增 lldap 写通道 + webhook + confirm-base-url
  + 两套 schedule + guard
- ldaplib 收拢为只读:属性常量/DN 归入 attrs.go;删 ops.go 写路径与 internal/password
- config.example.yml 重写成 LLDAP 形态
- 组命名修订(呼应 AGENTS.md 约束12):用部门稳定索引 cn=<dept_id> 做组身份,
  mirror_departments 增 group_id 列并跨快照保留,可读中文名只走 DB 展示
2026-09-29 23:48:49 +08:00

153 lines
4.8 KiB
Go

package ldaplib
import (
"fmt"
"strings"
"github.com/go-ldap/ldap/v3"
)
// Entry 是 LDAP 中一个条目的精简表示,用于与期望状态比对。
type Entry struct {
DN string
Attributes map[string]string
// multi 保留多值属性的完整值列表(如 uniqueMember),键为小写属性名。
multi map[string][]string
}
// Get 返回指定属性的第一个值。
func (e *Entry) Get(attr string) string {
return e.Attributes[strings.ToLower(attr)]
}
// NewEntry 构造一个 Entry。
//
// Entry 的字段对调用方是只读的(多值属性不导出),但同步逻辑与测试都需要
// 在内存中构造它,因此这里提供一个显式构造函数,而不是让调用方猜测内部结构。
func NewEntry(dn string, attrs map[string]string, members []string) *Entry {
e := &Entry{
DN: dn,
Attributes: make(map[string]string, len(attrs)),
multi: make(map[string][]string),
}
for k, v := range attrs {
e.Attributes[strings.ToLower(k)] = v
}
if len(members) > 0 {
e.multi[strings.ToLower(AttrUniqueMember)] = append([]string{}, members...)
e.Attributes[strings.ToLower(AttrUniqueMember)] = members[0]
}
return e
}
// userReadAttrs / groupReadAttrs 的定义见 attrs.go。
// ListUsers 读取某个 OU 下的全部用户条目(不递归)。
func (c *Conn) ListUsers(ouDN string) (map[string]*Entry, error) {
sr, err := c.conn.Search(ldap.NewSearchRequest(
ouDN, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=inetOrgPerson)", userReadAttrs, nil,
))
if err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
return map[string]*Entry{}, nil // OU 尚未创建,视为空
}
return nil, fmt.Errorf("读取用户列表失败 (%s): %w", ouDN, err)
}
out := make(map[string]*Entry, len(sr.Entries))
for _, e := range sr.Entries {
out[strings.ToLower(e.GetAttributeValue(AttrUID))] = entryFrom(e)
}
return out, nil
}
// ListGroups 读取指定子树下的全部分组条目,按 DN 小写为键。
// LLDAP 分组 objectClass 固定为 groupOfUniqueNames。
func (c *Conn) ListGroups(rootDN string) (map[string]*Entry, error) {
sr, err := c.conn.Search(ldap.NewSearchRequest(
rootDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(objectClass=%s)", groupObjectClass), groupReadAttrs, nil,
))
if err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
return map[string]*Entry{}, nil
}
return nil, fmt.Errorf("读取分组列表失败 (%s): %w", rootDN, err)
}
out := make(map[string]*Entry, len(sr.Entries))
for _, e := range sr.Entries {
out[strings.ToLower(e.DN)] = entryFrom(e)
}
return out, nil
}
// Members 返回分组条目的成员 DN 集合(全小写,便于比较)。
//
// 两种成员属性都识别:目录可能用 groupOfNames/member,也可能用
// groupOfUniqueNames/uniqueMember,取决于既有约定。写操作会使用配置指定的那一种,
// 但读取必须宽松,否则会漏掉另一类成员关系。
func (e *Entry) Members() map[string]struct{} {
out := make(map[string]struct{})
for _, attr := range memberAttrs {
for _, m := range e.multi[strings.ToLower(attr)] {
out[strings.ToLower(m)] = struct{}{}
}
}
return out
}
// ExistsDN 判断某个 DN 是否存在,用于幂等前置检查。
func (c *Conn) ExistsDN(dn string) (bool, error) {
sr, err := c.conn.Search(ldap.NewSearchRequest(
dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false,
"(objectClass=*)", []string{"dn"}, nil,
))
if err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
return false, nil
}
return false, fmt.Errorf("检查 DN 是否存在失败 (%s): %w", dn, err)
}
return len(sr.Entries) > 0, nil
}
// entryFrom 把 LDAP 条目转成精简表示。
// 单值属性存进 Attributes;多值属性原样保留在 multi 中。
func entryFrom(e *ldap.Entry) *Entry {
ent := &Entry{
DN: e.DN,
Attributes: make(map[string]string, len(e.Attributes)),
multi: make(map[string][]string),
}
// multiValuedAttrs 记录必须按多值处理的属性。
// 不能只凭「值个数 > 1」判断:一个只含 admin 的分组其 uniqueMember
// 只有一个值,若按单值属性存放,Members() 就会返回空集合,导致
// 成员比对完全失效(集成测试捕获过这个问题)。
multiValued := map[string]bool{
strings.ToLower(AttrMember): true,
strings.ToLower(AttrUniqueMember): true,
}
for _, a := range e.Attributes {
key := strings.ToLower(a.Name)
if multiValued[key] {
ent.multi[key] = append([]string{}, a.Values...)
if len(a.Values) > 0 {
ent.Attributes[key] = a.Values[0]
}
continue
}
if len(a.Values) == 1 {
ent.Attributes[key] = a.Values[0]
continue
}
ent.multi[key] = a.Values
if len(a.Values) > 0 {
ent.Attributes[key] = a.Values[0]
}
}
return ent
}