chore: 初始化 idsync 项目
飞书通讯录 ↔ LDAP 同步工具的首个提交。 - internal/feishu: 飞书通讯录客户端、快照与重试 - internal/ldaplib: LDAP 读写与批量操作 - internal/mapping: 飞书部门/用户到 LDAP 条目的映射 - internal/syncer: 同步计划生成、执行与调度 - internal/store: SQLite 台账 - internal/server + internal/webui: 管理后台与内嵌前端产物 - internal/password: SSHA 口令处理 - config.example.yml / Dockerfile / Makefile / web 前端源码 真实凭据(config.yml)、运行时数据(data/、backup/)与构建产物(bin/)不入库。
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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 是读取用户条目时需要取回的属性。
|
||||
// 刻意不读 userPassword:同步永远不需要比对或改写密码。
|
||||
var userReadAttrs = []string{
|
||||
AttrUID, AttrCN, AttrSN, AttrGivenName, AttrDisplayName, AttrMail,
|
||||
AttrMobile, AttrEmployeeNumber, AttrDepartmentNumber, AttrPostalAddress,
|
||||
AttrBusinessCategory, AttrEmployeeType, AttrDescription,
|
||||
}
|
||||
|
||||
// groupReadAttrs 读取分组时额外取回成员列表,以便检测成员漂移。
|
||||
// groupReadAttrs 读取分组时同时请求两种成员属性与 objectClass。
|
||||
// 缺省的属性不会报错,只是不返回,因此同时请求是安全的。
|
||||
var groupReadAttrs = []string{AttrCN, AttrDescription, AttrObjectClass, AttrMember, AttrUniqueMember}
|
||||
|
||||
// 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 小写为键。
|
||||
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)", c.group.ObjectClass), 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
|
||||
}
|
||||
|
||||
// EnsureOU 确保某个 OU 存在,不存在则创建。用于首次同步时准备 ou=Inactive 等。
|
||||
func (c *Conn) EnsureOU(dn, name string) error {
|
||||
exists, err := c.ExistsDN(dn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
add := ldap.NewAddRequest(dn, nil)
|
||||
add.Attribute(AttrObjectClass, ObjectClassOU)
|
||||
add.Attribute("ou", []string{name})
|
||||
if err := c.conn.Add(add); err != nil {
|
||||
if ldap.IsErrorWithCode(err, ldap.LDAPResultEntryAlreadyExists) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("创建 OU %s 失败: %w", dn, err)
|
||||
}
|
||||
return 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
|
||||
}
|
||||
Reference in New Issue
Block a user