# ERR-20260520-002: external_source 大小写错配——EntraSync 历史写入 'ENTRA' / 'LDAP' 大写，v2.4 SSO 严格比对小写，触发 unknown-externalSource 兜底 BINDING_CONFLICT

## 症状

SSO 登录走完 Microsoft + token exchange + ID token 校验全部 ✅，进 loginViaSSO 后被
"兜底视为冲突"分支抛：

```
warn [AuthService] 写 SSO audit 失败 action=SSO_BINDING_CONFLICT
```

backend 回 302 `/login?ssoError=SSO_BINDING_CONFLICT`，user 无法登入即使 email 完全匹配 +
Entra Object ID 实际等于 DB external_id。

## 根因

DB 现有 user `external_source = 'ENTRA'`（**大写**）— EntraSync 模块历史写入沿用
`UserSource` enum value 名（`ENTRA` / `LDAP`），按 PostgreSQL enum 习惯**全大写**。

v2.4 SSO `loginViaSSO` 实现严格小写比对：

```typescript
if (externalSource === 'entra') { ... }          // false: 'ENTRA' !== 'entra'
else if (externalSource === 'ldap') { ... }      // false
else {                                            // ← 进这里
  // externalSource 为其它值（按数据契约不应存在，但兜底视为冲突）
  throw new SsoError('SSO_BINDING_CONFLICT', `unknown externalSource=${externalSource}`);
}
```

v2.4 docs/modules/organization/06-data-model.md 写的取值约定是小写 'entra' / 'ldap' / NULL，
**跟 EntraSync 实际取值不一致**。docs-main 阶段 Recon 没扫到这个 drift。

## 修法（A + B 都做）

### A. Code 层归一化（容忍历史大写）

`backend/src/modules/organization/auth/auth.service.ts` loginViaSSO：

```typescript
// before
const externalSource = user.externalSource;

// after
const externalSource = (user.externalSource || '').toLowerCase();
```

并发回填 retry 分支同样改：`reExternalSource = (user.externalSource || '').toLowerCase()`。

新写入仍用小写（line 329 / 408 / 423 = 'entra'），保持向前兼容。

### B. DB 一次性 backfill 现有大写为小写

```sql
UPDATE platform_iam.users
SET external_source = LOWER(external_source)
WHERE external_source IS NOT NULL AND external_source != LOWER(external_source);
```

slot-1 跑结果：UPDATE 382 行。

## 防御

写新模块前的 Recon 应该 grep 历史数据真实取值，而不是只看 docs/schema 约定：

```bash
# 任何 string 类型字段（非 enum）的取值约定校验
docker exec <pg> psql -d <db> -c "
  SELECT DISTINCT external_source, COUNT(*)
  FROM platform_iam.users
  WHERE external_source IS NOT NULL
  GROUP BY external_source;
"
```

如果发现历史数据大小写 / 取值跟 docs 约定不一致，**必须先决策**：
- 改 docs 接受历史（添加归一化逻辑）
- 改历史向 docs 看齐（migration backfill）

不能假设 docs == DB。

## 关联

- 工单：#334 Entra ID SSO 接入
- 错配源：EntraSync 模块 backend/src/modules/organization/entra/entra.service.ts 写入策略
- 修复 PR：本 PR commit 6（SSO UI + bugfix bundle）
- 类似教训：`.learnings/2026-04-14-seed-upsert-drift.md`（运行时数据 ≠ source code 假设）
