Files
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

121 lines
4.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mapping
// 镜像 → 期望 LLDAP 状态 的落库化映射。
//
// 与旧 `Build(feishu.Snapshot)`(OpenLDAP 单阶段时代的嵌套 CN + inactive OU 模型)
// 不同,这里是目标架构的映射:输入是 SQLite 镜像的活动用户与活动部门,输出是
// 期望写入 LLDAP 的纯函数状态。LLDAP 组不能嵌套,组用部门稳定索引命名
// (cn=<dept_id>);用户 DN 固定挂在 ou=people 下,离职不靠 DN 移动而是
// 物理删除(见 syncer 阶段 6)。可读的部门中文名不进组名,只作展示。
import (
"sort"
"strings"
"idsync/internal/ldaplib"
"idsync/internal/store"
)
// DesiredGroup 是期望的分组(用部门稳定索引命名)。
type DesiredGroup struct {
DeptID string
// DN 是完整组 DN(来自 mirror_departments,形式 cn=<dept_id>,ou=groups,<base>)。
DN string
// Name 是部门中文名,仅展示用。
Name string
// Level 是部门在树中的层级,用于保证父组先于子组创建。
Level int
// Members 是期望成员的用户 DN 集合。
Members map[string]struct{}
}
// DesiredUser 是期望的用户。
//
// 不再有 InactiveDN:离职走物理删除,用户 DN 固定 ou=people/<base>。
type DesiredUser struct {
UID string
DN string
CN string
SN string
GivenName string
DisplayName string
Mail string
Mobile string
EmployeeNumber string
DepartmentNumber string
PostalAddress string
BusinessCategory string
Description string
DeptIDs []string
}
// DesiredState 是期望写入 LLDAP 的完整状态。
type DesiredState struct {
Groups []*DesiredGroup
Users []*DesiredUser
GroupByDeptID map[string]*DesiredGroup
}
// BuildFromMirror 由镜像的活动用户与活动部门构造期望状态。纯函数,不触碰 IO。
//
// 组名与 DN 直接复用镜像落库时算好的 PinyinName/DN(见 FlatDeptInfo),保证
// 物化与预览看到的是同一份命名,不会出现「写入了但预览里看不到」的分叉。
func BuildFromMirror(users []*store.MirrorUser, depts []*store.MirrorDepartment, baseDN string) *DesiredState {
st := &DesiredState{
Groups: make([]*DesiredGroup, 0, len(depts)),
Users: make([]*DesiredUser, 0, len(users)),
GroupByDeptID: make(map[string]*DesiredGroup, len(depts)),
}
peopleOU := "ou=people," + strings.TrimSpace(baseDN)
for _, d := range depts {
g := &DesiredGroup{
DeptID: d.DeptID,
DN: d.DN,
Name: d.Name,
Level: d.Level,
Members: make(map[string]struct{}),
}
st.Groups = append(st.Groups, g)
st.GroupByDeptID[d.DeptID] = g
}
for _, u := range users {
uid := u.UID
du := &DesiredUser{
UID: uid,
DN: ldaplib.UserDN(uid, peopleOU),
CN: uid,
SN: u.SN,
GivenName: u.GivenName,
DisplayName: u.DisplayName,
Mail: u.Email,
Mobile: u.Mobile,
EmployeeNumber: u.EmployeeNumber,
DepartmentNumber: u.DepartmentNumber,
PostalAddress: u.PostalAddress,
BusinessCategory: u.BusinessCategory,
Description: u.Description,
DeptIDs: u.DepartmentIDs,
}
st.Users = append(st.Users, du)
// 维护成员关系:用户只加入其实际所属且在本轮同步范围内的部门组。
for _, deptID := range u.DepartmentIDs {
if g, ok := st.GroupByDeptID[deptID]; ok {
g.Members[du.DN] = struct{}{}
}
}
}
// 分组按层级排序,父组必须排在子组之前(LLDAP 组虽不嵌套,但物化创建
// 顺序仍要保持父先于子,避免依赖隐含顺序造成问题)。
sort.SliceStable(st.Groups, func(i, j int) bool {
if st.Groups[i].Level != st.Groups[j].Level {
return st.Groups[i].Level < st.Groups[j].Level
}
return st.Groups[i].DeptID < st.Groups[j].DeptID
})
sort.SliceStable(st.Users, func(i, j int) bool { return st.Users[i].UID < st.Users[j].UID })
return st
}