- 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 展示
153 lines
4.8 KiB
Go
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
|
|
}
|