Files
BPOS-Service-Gateway/docs/keycloak-setup-log.md
T
lovefyfandClaude Opus 5 54beb5cf01 docs: Keycloak service 角色与 audience mapper 配置与验证记录
provisioning 服务账号缺 manage-realm 与 manage-clients,①③④ 均被 403 拒绝;
② 因角色未建而阻塞(权限探针返回 404 而非 403,证明补建角色后即可成功);
⑤ web-portal 的 aud-data-service 已存在,未做任何写操作。
已解码验证三个 token 的 aud / realm_access.roles,chaihao 的 Task 11 前置条件成立。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 06:37:16 +08:00

244 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Keycloak 配置与验证记录(N1 Task 10)
> **执行时间**:2026-08-13
> **目标 realm**:`corp` @ `https://keycloak.akhr-ai.com`
> **依据**:`docs/keycloak-agent-guide.md` §6(admin token)、§7.4/§7.5(角色与 mapper)、§10(验证)
> **结论**:**BLOCKED-partial** —— ⑤ 已存在无需变更;①③④ 因 provisioning 服务账号权限不足被 403 拒绝;② 因 ① 未完成而无法执行。
> **本记录不含任何 secret、密码或完整 token**,只记录操作与解码后的 `aud` / `realm_access.roles` claim。
---
## 0. 执行凭证与实际权限
按指南 §6.2 用 master realm 的 `provisioning` 服务账号(`client_credentials`)取 admin token,成功。
解码该 admin token 得到它在 `corp-realm` 管理 client 上的实际角色:
```json
{
"azp": "provisioning",
"resource_access": {
"corp-realm": {
"roles": ["manage-users", "view-users", "view-clients",
"query-clients", "query-groups", "query-users"]
}
}
}
```
这与指南 §6.1「一次性准备」里授予的角色集合一致(`manage-users` / `view-users` / `query-groups` / `view-clients`)。
对照指南 §6.3 权限表,**缺失两个角色**:
| 缺失角色 | 影响的操作 |
|---|---|
| `manage-realm`(连 `view-realm` 也没有) | ① 建 realm 角色 `service`;连 `GET /roles` 都是 403 |
| `manage-clients` | ③ ④ 给 client 加 protocol mapper |
> 按任务安全规则:**未尝试给自己或 provisioning 账号提权**,仅记录并上报。
### 只读探测到的现状(本次操作前)
| 对象 | UUID | 说明 |
|---|---|---|
| client `task-dispatcher` | `3e7c17c6-829e-43d1-95e5-aad5d8ae750e` | protocol mapper 数量 **0** |
| client `api-gateway` | `e02d6fc4-d463-49d8-bc24-4e85dde551d3` | protocol mapper 数量 **0** |
| client `web-portal` | `0e77f3a9-9a0c-4975-b441-14c183d12665` | 已有 5 个 mapper(见 ⑤) |
| client `data-service` | `feddbc60-4cc6-4e49-a1d8-a969fc4ea46f` | 本次未改动 |
| client `agent-service` | `b53cb9f3-3bad-4a10-bb9c-f2df8982e2ca` | 本次未改动(归智能体方) |
| SA `service-account-task-dispatcher` | `49a85fc7-020c-4098-ba38-af7b138129ea` | realm 角色:`user` / `offline_access` / `default-roles-corp` / `uma_authorization` |
| SA `service-account-api-gateway` | `e3f07791-7494-496a-953e-87f8bbc33fb5` | 同上 |
| 用户 `chaihao` | `96d21014-4e27-431e-a845-8d467bbad287` | enabled=true |
realm 现有角色(因 `GET /roles` 被 403,改用 `GET /users/{id}/role-mappings/realm/available` 侧面枚举,http=200):
`admin`、`user`、`offline_access`、`uma_authorization`(+`default-roles-corp`)。
**`service` 角色确认不存在。**
---
## 1. 逐项操作结果
### ① 建 realm 角色 `service` —— ❌ 403-missing-role
- 先 GET:`GET /admin/realms/corp/roles` → **403**(缺 `view-realm`);
改用 `GET /users/{sa}/role-mappings/realm/available` 确认 `service` 不存在,非重复创建。
- 写入:`POST /admin/realms/corp/roles`,body `{"name":"service","description":"服务间调用面角色"}`
→ **HTTP 403** `{"error":"HTTP 403 Forbidden"}`
- **缺失角色:`manage-realm`**(指南 §6.3「改 realm 设置」一行)。已跳过,继续其余操作。
### ② 把 `service` 挂到两个服务账号 —— ⏸ skipped(被 ① 阻塞)
- 前置检查 `GET /users/{uid}/role-mappings/realm/composite`(http=200):两个服务账号均**不含** `service`,确需授予。
- 因角色 `service` 尚不存在,**无法取得 role id**(授角色 body 必须是含 `id` 的角色对象数组,指南 §7.4),故未能执行。
- **权限探针(不产生任何变更)**:用一个不存在的 role id 发 `POST /users/{sa-task-dispatcher}/role-mappings/realm`
→ **HTTP 404 `{"error":"Role not found"}`**,**而不是 403**。
探针后复查 composite,角色集合未变,确认无副作用。
→ 结论:**本账号的 `manage-users` 足以完成 ②,② 唯一的阻塞点是 ①。**
只要 `service` 角色被建出来,② 可立即成功,不需要额外授权。
### ③ 给 `task-dispatcher` 加两个 audience mapper —— ❌ 403-missing-role
- 先 GET:`GET /clients/{uuid}/protocol-mappers/models` → http=200,**列表为空**,无同名 mapper,非重复创建。
- 写入两条(body 按指南 §7.5 形状,`protocolMapper=oidc-audience-mapper`,`access.token.claim=true`):
| mapper 名 | `included.client.audience` | 结果 |
|---|---|---|
| `aud-data-service` | `data-service` | **HTTP 403** |
| `aud-agent-service` | `agent-service` | **HTTP 403** |
- **缺失角色:`manage-clients`**(指南 §6.3「建/改 client、加 mapper」一行)。
### ④ 给 `api-gateway` 加 `aud-task-dispatcher` —— ❌ 403-missing-role
- 先 GET:`GET /clients/{uuid}/protocol-mappers/models` → http=200,**列表为空**,非重复创建。
- 写入 `aud-task-dispatcher`(`included.client.audience=task-dispatcher`)→ **HTTP 403**。
- **缺失角色:`manage-clients`**。
### ⑤ `web-portal` 加 `aud-data-service`(D7 选项 a)—— ✅ already-present,**未做任何写操作**
前置确认(要求:不得给人类授 admin,只做校验):
`GET /users?username=chaihao&exact=true` → id;`GET /users/{id}/role-mappings/realm/composite` → http=200:
```
admin, user, offline_access, default-roles-corp, uma_authorization
```
**chaihao 确实已持有 realm 角色 `admin`**,无需也未授予任何角色。
`GET /clients/{web-portal-uuid}/protocol-mappers/models` → http=200,已有 5 个 mapper:
| mapper 名 | 类型 | `included.client.audience` |
|---|---|---|
| `aud-api-gateway` | oidc-audience-mapper | `api-gateway` |
| `aud-agent-service` | oidc-audience-mapper | `agent-service` |
| `aud-task-dispatcher` | oidc-audience-mapper | `task-dispatcher` |
| **`aud-data-service`** | **oidc-audience-mapper** | **`data-service`** |
| `groups` | oidc-group-membership-mapper | — |
**目标 mapper `aud-data-service` 已存在**,按幂等原则未创建重复项,`web-portal` 配置**零改动**。
#### 共享 client 影响面说明(SHARED-CLIENT blast radius)
- 「`web-portal` 用户 token 的 `aud` 含 `data-service`」这一影响面**在本次任务之前就已存在**,
与指南 §1 末尾描述的「当前的受众模式:网关透传,`web-portal` 上挂了 4 个 audience mapper」完全一致。
**本次任务没有扩大任何影响面**,属于既有的、已文档化的架构决策。
- 影响面本身仍然成立并在此记录备查:**每一个经 `web-portal` 登录的用户**,其 token 的 `aud`
都包含 `data-service`(以及另外 3 个后端)。代价是任一持有该 token 的下游服务技术上也能调其它服务。
- 收紧方式(指南 §1 / §8.3):把对应 audience mapper 从 `web-portal` 摘掉,改由 `api-gateway`
用 token exchange 单独换发。这是局部改动,不需要重构。
---
## 2. 验证(指南 §10,解码真实 token,不看 2xx)
### V1 `task-dispatcher` client_credentials —— ❌ FAIL(预期内,③② 未生效)
secret 于运行时从 `BPOS-Service-Task/.env` 读取,未落盘、未复制到任何位置。
```json
{
"azp": "task-dispatcher",
"preferred_username": "service-account-task-dispatcher",
"aud": "account",
"realm_access": { "roles": ["default-roles-corp", "offline_access", "uma_authorization", "user"] }
}
```
- `aud` 期望含 `data-service` + `agent-service` → **实际只有 `account`,不满足**。
- `realm_access.roles` 期望含 `service` → **不含,不满足**。
### V2 `api-gateway` client_credentials —— ❌ FAIL(预期内,④② 未生效)
secret 于运行时从 `BPOS-Service-Gateway/.env` 读取,未落盘、未复制到任何位置。
```json
{
"azp": "api-gateway",
"preferred_username": "service-account-api-gateway",
"aud": "account",
"realm_access": { "roles": ["default-roles-corp", "offline_access", "uma_authorization", "user"] }
}
```
- `aud` 期望含 `task-dispatcher` → **实际只有 `account`,不满足**。
- `realm_access.roles` 期望含 `service` → **不含,不满足**。
### V3 `chaihao` 经 `web-portal` —— ✅ PASS(用 evaluate-scopes 预览,非 password grant)
**密码模式不可用**:`grant_type=password` + `client_id=web-portal` 返回
`{"error":"unauthorized_client","error_description":"Client not allowed for direct access grants"}`。
查 client 配置确认这是**正确的现状**,不是故障:
```json
{ "clientId": "web-portal", "publicClient": true,
"standardFlowEnabled": true, "directAccessGrantsEnabled": false }
```
与指南 §1「两个前端只开 standard flow」、§12-3「不要用密码模式做生产登录」一致。
因此改用指南 §7.5 / §10 认可的预览端点
`GET /clients/{web-portal-uuid}/evaluate-scopes/generate-example-access-token?scope=openid&userId={chaihao}`(http=200):
```json
{
"azp": "web-portal",
"preferred_username": "chaihao",
"aud": ["agent-service", "api-gateway", "data-service", "task-dispatcher", "account"],
"realm_access": { "roles": ["default-roles-corp", "offline_access", "admin", "uma_authorization", "user"] }
}
```
- `aud` 含 **`data-service`** ✅
- `realm_access.roles` 含 **`admin`** ✅
- **Task 11 前置条件成立**:chaihao 经 `web-portal` 登录后,可通过 `requireRealmRole('admin')` + `aud=data-service` 的建域接口。
- ⚠️ 说明:这是同一套 mapper 管线生成的预览 token,不是浏览器真实登录换来的 token。
真实 token 需走授权码+PKCE(指南 §8.1)。Task 11 若要用真 token,需从前端登录后取,
或由人类在浏览器登录后复制 token。
---
## 3. 阻塞与解除办法(需人类在服务器侧执行)
当前 provisioning 服务账号缺 `manage-realm` 与 `manage-clients`。**agent 不得自行提权**,请人类按指南 §6.1 的方式补授:
```bash
cd /opt/keycloak
PROV=$(sudo ./kc kcadm get clients -r master -q clientId=provisioning \
--fields id --format csv --noquotes | tail -1)
SA=$(sudo ./kc kcadm get "clients/$PROV/service-account-user" -r master \
--fields id --format csv --noquotes | tail -1)
# 补授两个角色(最小权限:只补这两个,不要授 realm-admin —— 指南 §12-5)
sudo ./kc kcadm add-roles -r master --uid "$SA" \
--cclientid corp-realm --rolename manage-realm --rolename manage-clients
```
补授后,①②③④ 可按本文记录的 body 原样重跑,② 已由探针证明会成功(当前是 404 而非 403)。
也可由人类直接在服务器侧一次做完(不依赖 provisioning 账号):
```bash
cd /opt/keycloak && sudo ./kc login
# ① 建角色
sudo ./kc kcadm create roles -r corp -s name=service -s 'description=服务间调用面角色'
# ② 挂到两个服务账号
for C in task-dispatcher api-gateway; do
U=$(sudo ./kc kcadm get clients -r corp -q clientId=$C --fields id --format csv --noquotes | tail -1)
SAU=$(sudo ./kc kcadm get "clients/$U/service-account-user" -r corp --fields id --format csv --noquotes | tail -1)
sudo ./kc kcadm add-roles -r corp --uid "$SAU" --rolename service
done
# ③④ 加 audience mapper(三条,body 见本文 §1)
```
---
## 4. 交接与注意事项
- **智能体方(agent-service)**:`agent-service` 自身的 audience 配置归智能体团队,本次**仅知会、未触碰**。
该 client 的 UUID 为 `b53cb9f3-3bad-4a10-bb9c-f2df8982e2ca`,当前未做任何改动。
待 ③ 完成后,`task-dispatcher` 的服务账号 token 才会带上 `aud=agent-service`,
智能体方需确认自己按指南 §5.3 校验 `aud` 含 `agent-service`。
- **`data-service` 不配 secret 是正确的**(指南 §5.2/§12-4),本次未给它加任何凭据。
- **N1 真 token 验收(Task 11)**:
- 走 chaihao / `web-portal` 的人类管理员路径 → **前置已满足**,可执行。
- 走 `task-dispatcher` / `api-gateway` 服务间调用路径 → **前置未满足**,须先解除 §3 的权限阻塞。
- 本次共发生 **4 次 403**(① 建角色 1 次、③ mapper 2 次、④ mapper 1 次)+ **1 次只读 403**(`GET /roles`),
均已按安全规则记录并跳过,**未做任何提权尝试**,realm 状态相对本次操作前**零变更**。