- 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 展示
121 lines
4.0 KiB
Go
121 lines
4.0 KiB
Go
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
|
||
} |