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

12 KiB
Raw Blame History

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 上的实际角色:

{
  "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 读取,未落盘、未复制到任何位置。

{
  "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 读取,未落盘、未复制到任何位置。

{
  "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 配置确认这是正确的现状,不是故障:

{ "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):

{
  "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 的方式补授:

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 账号):

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 状态相对本次操作前零变更。