Files
caowangrenbo 6d31cba631 docs: 交付收尾(README/Dockerfile/Makefile 对齐 + 移除迁移状态节)
- README 重写:两阶段架构、LDAP 读/GraphQL 写、部门索引组名、离职双确认、
  部署(起 LLDAP + Authelia 官方 lldap 模板 + 配 idsync + 构建运行)、命令、测试
- Makefile:test-integration 改指本地 LLDAP(3890/17170),新增 make lldap 起容器,
  头部注释更新为 LLDAP
- AGENTS.md:移除「迁移状态」节(迁移已完成,旧实现已删除/收敛)
2026-09-29 23:58:29 +08:00

7.2 KiB
Raw Permalink Blame History

idsync — 飞书通讯录 → LLDAP 同步器

把飞书通讯录同步到 LLDAP,替代 OpenLDAP 时代的 go-ldap-admin。数据单向流动、两阶段:

飞书通讯录 ─快照(低频)─▶ SQLite 镜像 ─物化(略高频)─▶ LLDAP(只在职)
                          (全字段+部门关系+生命周期)   │
                                                      ▼
                         离职:机器确认(飞书 is_resigned) → 飞书卡片一次性端点
                         人工确认 → confirmed_leave → 物化时物理删除
  • 读走 LDAP、写走 GraphQL:LLDAP 的实际状态用 LDAP 读,所有写入(建/改/删用户、 建组、成员关系)走 GraphQL,密码由 LLDAP 独占(程序绝不写密码,首次登录走 Authelia「忘记密码」)。
  • 本工具不参与认证链路:登录、会话、密码重置、发信都由 Authelia 负责。它只决定 「飞书有什么数据、LLDAP 该长什么样、两者差异是什么」。它对外发出的唯一通知是 「离职人工确认」飞书卡片。

定位与边界

属于本项目 不属于本项目
飞书有什么数据、LDAP 该长什么样、两者差异是什么 谁有权限登录(Authelia)
快照/物化调度、离职双确认状态机、离职卡片通知 认证、会话、登录页、密码重置

鉴权链是 业务系统 → Authelia → LLDAP bind;本工具只保证 LLDAP 里只在职、且离职 必须经人工确认后才删除。

架构

internal/config     配置加载、默认值、校验、DN 拼接、两套 schedule
internal/feishu     只读飞书:拉通讯录、机器确认离职、发离职确认卡片(webhook)
internal/store      SQLite 镜像:全字段用户 + 部门关系 + 生命周期状态 + 一次性 token
internal/mapping    镜像 → 期望 LLDAP 状态(纯函数);组名=部门稳定索引 cn=<dept_id>
internal/ldaplib    LLDAP 读原语:连接/bind/搜索
internal/lldapgql   LLDAP 写通道:GraphQL(登录 + 各 mutation),端点 /api/graphql
internal/syncer     diff 引擎(纯函数)+ 快照/物化编排(单实例互斥)
internal/server     HTTP 管理界面 + 一次性离职确认端点(token 即凭证)
internal/webui      go:embed 前端产物 + ETag(内容指纹)

关键约定

  • 只在职:LLDAP 只有 ou=people,uid 取企业邮箱 @ 前缀;缺少企业邮箱的用户 被跳过并记入 skipped(WebUI 可查,必须被管理者发现)。
  • 组名 = 部门稳定索引:组 displayName/CN = cn=<open_department_id>,<ou=groups> 。用部门 ID 而非拼音——改名/挪动不改变组身份,避免「改名波及子孙组」;可读的部门 中文名只存在数据库(mirror_departments.name / 用户 business_category),不进入 LDAP 组名。
  • 离职 = 双确认后才删:缺席 → 飞书 is_resigned 机器确认 → 生成一次性 token 并发 飞书管理群卡片(按钮跳本工具确认端点)→ 人工确认 → confirmed_leave → 下一轮物化 物理删除。两道闸缺一不可;用户重新可见即撤销并作废未用 token。
  • 操作顺序是硬约束:分组 → 用户 → 加组 → 移组 → 删除(最后一阶段,且只处理 confirmed_leave)。
  • 所有权只认镜像台账:镜像之外的条目(服务账号、手工账号、admin)永不入计划。
  • 快照与物化互斥:共享一把单实例锁,绝不并发,防止物化读到半新半旧镜像污染 LLDAP。

部署

1. 起 LLDAP

podman run -d --name idsync-lldap \
  -e LLDAP_LDAP_BASE_DN=dc=example,dc=com \
  -e LLDAP_JWT_SECRET='<random>' \
  -e LLDAP_LDAP_USER_PASS='<admin-pass>' \
  -p 3890:3890 -p 17170:17170 \
  ghcr.io/lldap/lldap:stable

LDAP 明文 3890、TLS 6360;WebUI/GraphQL 17170。用户名默认 admin。

2. 配 Authelia(官方 lldap 模板,勿定制)

authentication_backend:
  ldap:
    implementation: lldap
    url: ldaps://lldap:6360   # 或 ldap://lldap:3890 + start_tls
    base_dn: dc=example,dc=com
    # 专用 bind 用户,别拿 admin
    user: uid=authelia_svc,ou=people,dc=example,dc=com
    password: <bind-pass>
    groups_filter: (&(memberOf={dn})(objectClass=groupOfUniqueNames))
    attributes:
      username: uid
      display_name: cn
      mail: mail
      member_of: memberOf

给 Authelia 专用 bind 用户授予 lldap_strict_readonly + lldap_password_manager。 用户 objectClass 是 inetOrgPerson,官方 users_filter(objectClass=person)即命中, 不要添加任何自定义属性/过滤。mail 必须是企业邮箱(Authelia 重置密码的收件地址)。

3. 配 idsync

复制 config.example.yml → config.yml,填:飞书 app 凭据 + 离职卡片 webhook、 LLDAP 读(LDAP URL/base-dn/admin)+ 写(GraphQL base-url/admin)、离职确认端点对外 server.confirm-base-url、两套 schedule。其中:

  • confirm-base-url 必须是公网/反代可达地址,飞书卡片按钮链接用它拼接。
  • 先观察几轮 sync-dry-run: true 的物化计划,再放开为实际执行。

4. 构建与运行

宿主无 Go/Node,构建都在容器内(Makefile 已封装,容器强制 --network=host):

make deps / make css / make build / make run

产物 bin/idsync 为静态二进制,内嵌 WebUI,可按 Dockerfile 打进镜像部署到 0.0.0.0:8899。镜像数据库在 data/(含离职状态与一次性 token,务必持久化)。

命令

./idsync -check-config        # 打印有效配置(凭据脱敏)
./idsync -snapshot            # 只跑一轮快照(飞书→镜像+离职流程)后退出
./idsync -dry-run             # 快照+物化,只打印计划不写入
./idsync -apply               # 快照+物化并实际写入
./idsync -apply -force        # 跳过删除数量阈值护栏
./idsync -gen-secret          # 生成会话密钥
./idsync -hash-password '…'   # 生成管理员密码 bcrypt 哈希

管理界面 http://<host>:8899:概览(连通性/待确认离职/被跳过)、用户、部门分组、 变更预览(含「立即快照」「应用变更」)、物化历史。

测试

make lldap           # 拉起本地 LLDAP 容器(可选)
make test            # 单元测试(无需外部依赖)
make test-integration# 对真实 LLDAP 运行(3890/17170)
make vet             # go vet

单元测试覆盖:镜像事务化回滚、生命周期状态机、一次性 token(一次/过期/复用被拒)、 diff 引擎的安全回归(未确认不删、非受管不删、admin 永不移出、删除最后阶段)、 部门索引组命名稳定。集成测试用假飞书 + 真 LLDAP 走通全链路:首次创建、幂等、属性漂移、 confirmed_leave 删除、重新入职重建。

项目状态

两阶段迁移已完成:SQLite 镜像层、飞书卡片 + 快照、镜像驱动映射(部门索引组名)、 GraphQL 写客户端、config/ldaplib 收敛为 LLDAP 形态、物化编排、一次确认端点 + WebUI、 集成测试改用真 LLDAP 容器。make test / make test-integration / make vet 全绿。