# SOC2 邮件组授权 运维手册

> 适用范围:SOC2 合规要求的「飞书发起 + IT 审批 → 自动加入 `soc2@pingcap.com` Google Group」外部审批流的排查与对账
> 最近更新:2026-06-08(首版 — 沉淀 migration 149 外部起点审批流 + 加组排查路径)

---

## 零、一句话说清这个流程

这是一条**外部起点(external-origin)审批流**:申请人和审批**全部在飞书里完成**,Helix 没有申请页面。Helix 做的唯一一件事 —— 当飞书把审批实例推成 `APPROVED` 时,把申请人邮箱加进 `soc2@pingcap.com` 这个 Google Group(带离职拦截),并写审计。

**没有定时任务驱动加组。** 它是飞书 webhook **即时触发**的,正常是秒级生效。所以"为什么还没加进去"几乎从来不是"需要等 Helix 跑批" —— 要么是飞书 webhook 没到 / 实例还没终态,要么是 Helix 某步失败,要么是 Helix 已经加完、只是 **Google 侧传播延迟**。

```mermaid
sequenceDiagram
    actor AP as 申请人
    participant FS as 飞书审批
    actor IT as IT 审批人
    participant HX as Helix
    participant GG as Google Workspace

    AP->>FS: 提交「SOC2 加邮箱」(模板 9A42C433-…)
    FS->>IT: 流转到 IT
    IT->>FS: 审批通过
    FS-->>HX: webhook approval_instance APPROVED
    Note over HX: _synthesize_external_origin_request<br/>(accept_external_origin=TRUE) + 解析申请人邮箱
    alt 在职
        HX->>GG: members.insert(soc2@pingcap.com)
        GG-->>HX: 200 OK / 409 已是成员(幂等)
        Note over HX: 审计 soc2_email_group_grant.member_added
    else leaver_pending / left
        Note over HX: 拒绝 → SOC2_APPLICANT_LEFT 409<br/>审计 soc2_email_group_grant.refused_leaver
    end
```

> 同一张图同时渲染在应用内帮助中心(IT 管理员 tab → 锚点 `#soc2-email-group`)和架构文档 `docs/architecture/PLATFORM.md`。三处改流程要同步更新。

---

## 一、涉及的代码与配置

| 项 | 位置 |
|----|------|
| 加组处理器 | [`server/app/domains/soc2_email_group/service.py`](../../server/app/domains/soc2_email_group/service.py) |
| 外部起点 webhook 补建 | `ApprovalService._synthesize_external_origin_request` in [`server/app/approval/service.py`](../../server/app/approval/service.py) |
| Google 加组底层调用 | `GoogleWorkspaceDriver._group_add` in [`server/app/integrations/drivers/google_workspace.py`](../../server/app/integrations/drivers/google_workspace.py) |
| 流配置 | `integration_approval_flows` 行:`flow_key='soc2_email_group_grant'`、`external_code='9A42C433-6A4B-411C-9B2E-9182BEEBC6BD'`、`accept_external_origin=1`(migration 149 seed) |
| 目标组 | `soc2@pingcap.com`(常量 `SOC2_GROUP_EMAIL`) |
| Google SA scope | `admin.directory.group.member`(复用「建邮件组」用的同一个 Google Workspace 集成) |
| 回归测试 | [`server/tests/test_approval/test_external_origin_callback.py`](../../server/tests/test_approval/test_external_origin_callback.py) |

---

## 二、排查:"我在飞书审批了,人还没进组"

按这个顺序查,一步定位。所有查询直连 prod TiDB(连接串见根目录 `.env` 注释行)。

### 1. 先确认 Helix 到底加没加成功 —— 查审计

```sql
SELECT created_at, action, status, detail
FROM audit_logs
WHERE detail LIKE '%<申请人邮箱>%'
  AND (action LIKE 'soc2_email_group_grant%' OR action LIKE 'approval.soc2%')
ORDER BY created_at DESC LIMIT 10;
```

- 看到 `soc2_email_group_grant.member_added` + `status=success` + `detail.external_ref=<一串数字>`
  → **Helix 已经加成功了**。`external_ref` 是 Google 在 `members.insert` 成功后返回的成员 ID,等于 Google 已接受写入。此时人没"看到"只可能是 **Google 侧传播延迟**(见第三节),不是 Helix 问题。
- 看到 `soc2_email_group_grant.refused_leaver`
  → 申请人被**离职拦截**了(状态 `leaver_pending` / `left`)。这是设计行为,不是 bug。先核实人事状态。
- 看到 `...member_added status=failure`(或一条 `SOC2_GROUP_ADD_FAILED`)
  → Google 调用真失败了。看 `detail` 里的 Google 错误。最常见是 SA 缺 `admin.directory.group.member` scope(403),或目标组邮箱写错。
- **只看到 `approval.soc2_email_group_grant.pending`、迟迟没有 approved/member_added**
  → 飞书还没把实例推成 `APPROVED`(见下一步)。

### 2. 没有 member_added,只有 pending —— 查审批实例状态

```sql
SELECT id, external_id, status, applicant_id, created_at, updated_at
FROM approval_requests
WHERE approval_type='soc2_email_group_grant'
ORDER BY created_at DESC LIMIT 10;
```

- `status='pending'`:Helix 这边还停在待审。两种原因:
  1. **飞书审批流不止 IT 一个节点** —— 你点的那一下不是最后一个审批人,实例还没到 `APPROVED`。webhook 只在实例**终态**才带 APPROVED。
  2. **webhook 丢了**(飞书是 at-most-once 投递,丢包/5xx 重试耗尽都可能)。有兜底:管理员在审批详情点「刷新流程」会 `replay_callback_from_feishu` 幂等重放;`approval_pending_reaper` cron 每天也会对飞书已终态的实例补一次。所以最坏情况是**等到次日 reaper**,但手动刷新可立即触发。
- `status='approved'` 但第 1 步没有 member_added:罕见,基本不会发生(approved 是在加组处理器跑完之后才写的)。真遇到了带 `trace_id` 去 worker/app 日志查。

### 3. Helix 已加成功,但 Google 里看不到 —— Google 侧最终一致性

`member_added=success` + 有 `external_ref` = Google 已接受写入。剩下的延迟全在 Google 自己:

- **admin.google.com → 群组 → 成员列表**:通常几秒 ~ 1–2 分钟可见
- **groups.google.com 网页视图**:刷新更慢
- **邮件投递开始认这个新成员**:可能再延后一两分钟

处理:去 admin.google.com 搜申请人邮箱核实;等 1–2 分钟刷新。`external_ref` 在手基本不可能丢 —— 若 10 分钟后 admin 控制台仍搜不到该成员,才需要查 Google 侧(配额/同步),而不是 Helix。

### 4. 真实案例(2026-06-08)

minjea.seo@pingcap.com:06-04 在飞书提交,06-08 02:30:13 UTC 审批通过,**同一秒** Helix 加组成功(`member_added`,`already_member=false`,`external_ref=117592134897301221130`),`employment_status=active` 未触发离职拦截。"还没进组"的体感来自 Google 控制台传播延迟,非 Helix 问题。

---

## 三、关键设计点(别误判成 bug)

- **离职拦截是 SOC2 取证点,不是 bug。** 离职/离职中的人被拒绝加组,会留 `refused_leaver` 审计。
- **幂等。** Google 返回 409(已是成员)按成功处理;飞书重复推 webhook 不会重复加组、不会重复写审计(idempotency guard 覆盖 `approved/rejected/cancelled/completed`)。
- **不走 provisioning 队列。** 一次性加组,不是计费坐席 —— 无 plan、无 allocation 行、无月账单。审计行 + `approval_requests` 行就是唯一真源,没有"软件坐席"那套记录。
- **邮箱匹配大小写/别名不敏感。** 解析申请人时按 `lower(work_email)` 或 `email_aliases` 命中 `identity_persons`;匹配不到 Helix Person 也会继续加组(只是 `applicant_person_id=null`),不阻断。
