- README 重写:两阶段架构、LDAP 读/GraphQL 写、部门索引组名、离职双确认、 部署(起 LLDAP + Authelia 官方 lldap 模板 + 配 idsync + 构建运行)、命令、测试 - Makefile:test-integration 改指本地 LLDAP(3890/17170),新增 make lldap 起容器, 头部注释更新为 LLDAP - AGENTS.md:移除「迁移状态」节(迁移已完成,旧实现已删除/收敛)
152 lines
7.2 KiB
Markdown
152 lines
7.2 KiB
Markdown
# idsync — 飞书通讯录 → LLDAP 同步器
|
||
|
||
把飞书通讯录同步到 [LLDAP](https://github.com/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
|
||
|
||
```bash
|
||
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 模板,勿定制)
|
||
|
||
```yaml
|
||
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`):
|
||
|
||
```bash
|
||
make deps / make css / make build / make run
|
||
```
|
||
|
||
产物 `bin/idsync` 为静态二进制,内嵌 WebUI,可按 `Dockerfile` 打进镜像部署到
|
||
`0.0.0.0:8899`。镜像数据库在 `data/`(含离职状态与一次性 token,务必持久化)。
|
||
|
||
## 命令
|
||
|
||
```bash
|
||
./idsync -check-config # 打印有效配置(凭据脱敏)
|
||
./idsync -snapshot # 只跑一轮快照(飞书→镜像+离职流程)后退出
|
||
./idsync -dry-run # 快照+物化,只打印计划不写入
|
||
./idsync -apply # 快照+物化并实际写入
|
||
./idsync -apply -force # 跳过删除数量阈值护栏
|
||
./idsync -gen-secret # 生成会话密钥
|
||
./idsync -hash-password '…' # 生成管理员密码 bcrypt 哈希
|
||
```
|
||
|
||
管理界面 `http://<host>:8899`:概览(连通性/待确认离职/被跳过)、用户、部门分组、
|
||
变更预览(含「立即快照」「应用变更」)、物化历史。
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
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` 全绿。 |