package syncer // 物化执行器:把 diff 计划通过 GraphQL 写落到 LLDAP。 // // 这里是两阶段架构里唯一写 LLDAP 的执行路径;读 LLDAP 走 ldaplib(见 plan 的 // ActualUsers/ActualGroups)。组间成员关系用 LLDAP 内置 addUserToGroup,groupId // 来自 material 开始时通过 GraphQL 拉取的 displayName→id 映射,不凭名字反推。 import ( "context" "fmt" "log/slog" "idsync/internal/lldapgql" ) // ExecConfig 是执行器所需的环境参数。 type ExecConfig struct { PeopleOU string } // Result 是执行结果统计。 type Result struct { Created int Updated int Deleted int Added int Removed int Failed int Errors []string } // Executor 把计划应用到 LLDAP(写走 GraphQL)。 type Executor struct { gql *lldapgql.Client cfg ExecConfig log *slog.Logger } // NewExecutor 构造执行器。 func NewExecutor(gql *lldapgql.Client, cfg ExecConfig, log *slog.Logger) *Executor { return &Executor{gql: gql, cfg: cfg, log: log} } // Apply 执行计划。任何一步失败都会被记录并继续处理后续操作——一次物化中某个 // 用户的数据问题不应阻断其余几百人的同步。groupIDs 是 dept_id→groupId 映射, // 会在创建新组时被原地扩充。 func (e *Executor) Apply(ctx context.Context, plan *Plan, groupIDs map[string]int) *Result { res := &Result{} for _, op := range plan.Ops { if err := ctx.Err(); err != nil { res.Errors = append(res.Errors, "物化被取消: "+err.Error()) return res } if err := e.applyOne(ctx, op, groupIDs); err != nil { res.Failed++ msg := fmt.Sprintf("%s(%s): %s", op.Kind, op.Target, err.Error()) res.Errors = append(res.Errors, msg) e.log.Error("物化操作失败", "kind", op.Kind, "target", op.Target, "err", err) continue } switch op.Kind { case OpCreateUser, OpCreateGroup: res.Created++ case OpUpdateUser: res.Updated++ case OpDeleteUser: res.Deleted++ case OpAddMember: res.Added++ case OpRemoveMember: res.Removed++ } } return res } // applyOne 执行单个操作。 func (e *Executor) applyOne(ctx context.Context, op Op, groupIDs map[string]int) error { switch op.Kind { case OpCreateGroup: // LLDAP 组 displayName 即部门索引;创建后把返回的 groupId 记进映射, // 供后续成员操作用。 name := op.Group.DeptID id, err := e.gql.CreateGroup(ctx, name) if err != nil { return err } groupIDs[name] = id return nil case OpCreateUser: // 创建用户不设密码:密码由 LLDAP 独占、首次登录走 Authelia 忘记密码。 return e.gql.CreateUser(ctx, &lldapgql.CreateUserInput{ ID: op.User.UID, Email: sp(op.User.Mail), DisplayName: sp(op.User.DisplayName), FirstName: sp(op.User.GivenName), LastName: sp(op.User.SN), }) case OpUpdateUser: // 省略即不改:这里显式带全四个可写硬字段,与 attrsDiffer 判定的差异一致。 return e.gql.UpdateUser(ctx, &lldapgql.UpdateUserInput{ ID: op.User.UID, Email: sp(op.User.Mail), DisplayName: sp(op.User.DisplayName), FirstName: sp(op.User.GivenName), LastName: sp(op.User.SN), }) case OpAddMember: gid, ok := groupIDs[op.Group.DeptID] if !ok { return fmt.Errorf("未在 groupId 映射中找到部门 %s 的组", op.Group.DeptID) } return e.gql.AddUserToGroup(ctx, op.MemberUID, gid) case OpRemoveMember: gid, ok := groupIDs[op.Group.DeptID] if !ok { return fmt.Errorf("未在 groupId 映射中找到部门 %s 的组", op.Group.DeptID) } return e.gql.RemoveUserFromGroup(ctx, op.MemberUID, gid) case OpDeleteUser: // LLDAP deleteUser 会级联清理成员关系(外键 CASCADE),无需先移组再删。 return e.gql.DeleteUser(ctx, op.Target) default: return fmt.Errorf("未知操作类型: %s", op.Kind) } } // sp 生成 *string。 func sp(s string) *string { return &s }