ADR 0008: Client ロール(権限)の自助サービス管理 — Console が書き、アプリケーションはトークンから読むだけ
- Status: Proposed
- Date: 2026-10-15
Context
Section titled “Context”issue #58 / ADR 0006(PR #124・#125)で、Identity Console に Client(OIDC)の自助サービス作成機能が入った。他チームは Keycloak Admin Console に直接ログインせずに、自分のアプリ用の Client を作れるようになった。
一方で、作った Client に「どんな権限(ロール)があるか」を設計・管理する手段は一切ない。これは見落としではなく、ADR 0006 が明示的に決めた Non-Goal:
Client scope・protocol mapper・role mapping の個別管理 UI:決定1の固定設定以外、一切の調整 UI を持たない。
(決定1について)「何も割り当てない」と決めているのは role scope mapping のことであり、こちらは第一フェーズで一切付与しない。明示的なロールスコープ割り当て UI は第一フェーズの範囲外(issue #58 の non-goal)。
この状態で他チームが自分のアプリに「閲覧者/管理者」程度の権限区分を持たせたい場合、選択肢は実質2つしかない: (a) Keycloak Admin Console に直接触って client role を手で作る——Console が自助化しようとした目的と矛盾する、(b) アプリ側で独自に権限テーブル・権限管理 UI を一から設計する。docs/integration.md(他チーム向け連携ガイド)は既に
Keycloak の realm ロールやクライアントロールはデフォルトで access token の
realm_access.roles/resource_access.<client>.rolesに含まれる(追加のスコープ設定不要)
と、読み取り側は案内済み。欠けているのは「ロールをどう定義・付与するか」という書き込み側だけ(issue #196)。
既存の近い実装(そのままでは使えない)
Section titled “既存の近い実装(そのままでは使えない)”Console には「Console ロールの付与・剥奪」機能(issue #167、console/src/features/consoleroles/・identity-admin/.../consoleroles/)が既にある。契約は GET/PUT /api/console/v1/users/{userId}/console-roles(ユーザー起点、ロール集合を丸ごと置き換える)。だが:
ConsoleRoleService/ConsoleRoleMutationServiceは対象の client をidentity-consoleに決め打ちしている。- 割り当て可能なロール名は Kotlin の固定 enum
ConsoleRole.entries(viewer/user-admin/security-admin/platform-admin/org-client-admin)—実行時に新しいロール名を定義する経路がない。 - 書き込みに必要な権限は
Permission.ADMIN_USERS_MANAGE(“replace another user’sidentity-consoleclient roles from the Console” と doc comment に明記)——これもidentity-console専用の名前・意味で設計されている。
つまりこの機能は「定義済みのロールをユーザーに割り当てる」ことしかできず、しかも対象は identity-console 一つだけ。今回必要なのは「任意の自分の Client の上に新しいロール名を定義し、それをユーザーに割り当てる」こと——似ているが同じではない。
Decision
Section titled “Decision”1. 責務分担の原則: Console が唯一の書き込み元、アプリケーションは読み取り専用
Section titled “1. 責務分担の原則: Console が唯一の書き込み元、アプリケーションは読み取り専用”- ロールの定義(名前の作成・削除)とユーザーへの付与・剥奪は、どちらも Console(identity-admin BFF)経由のみで行える。アプリケーション自身が自分のロールを読み書きする API はこのプラットフォームに存在しない。
- アプリケーションは OIDC access token の
resource_access.<clientId>.rolesclaim を読むだけ——新しい読み取り API は不要(これは既存の Keycloak のデフォルト動作で、docs/integration.mdに既に記載済み)。 - この非対称性(書き込みは Console だけ、アプリは読むだけ)は意図的な設計: アプリケーション自身に自分の権限を書き換える手段を与えると、サーバー側検証なしで権限昇格が起きる経路になる。ADR 0006 が「呼び出し側に任意の
ClientRepresentationや protocol mapper、role mapping を渡させてはいけない」としている原則をここでも踏襲し、監査可能な唯一の書き込み元を Console に固定する。
2. データモデル: Keycloak の client role をそのまま正本にする、新しい永続化は作らない
Section titled “2. データモデル: Keycloak の client role をそのまま正本にする、新しい永続化は作らない”- ロールの正本は Keycloak の client role(
/admin/realms/{realm}/clients/{id}/roles)。Organizations と同じ理由(Keycloak が既に正本を持っている)で、identity-admin 側の DB に複製しない。 - ロール名の制約: Keycloak 自体の制約に加え、サーバー側で
^[a-z][a-z0-9-]{0,49}$程度の形式検証を行う(Client のclientIdに対する既存の検証方針と同様、Keycloak の検証に丸投げしない)。 descriptionは任意フィールドとして Keycloak の role representation にそのまま保存する(Console UI 上での人間向けラベル用途。権限判定には使わない)。
3. Permission モデルの拡張
Section titled “3. Permission モデルの拡張”- 新設:
CLIENTS_ROLES_WRITE("clients:roles:write")/CLIENTS_ROLES_WRITE_OWN_ORG("clients:roles:write:own-org")。既存のCLIENTS_WRITE/CLIENTS_WRITE_OWN_ORGと同じ対構造にする。- 却下した代替案: 既存の
CLIENTS_WRITEをそのまま流用する。「Client の接続設定(redirect URI 等)を変える」ことと「その Client に誰が何の権限を持つか決める」は性質の異なる操作であり、将来どちらかだけを別の担当者に委譲したくなる可能性がある(例: org-client-admin にはロール付与だけ開放し、redirect URI 変更は引き続き内部管理者限定にする、等)。初版から分けておけば、権限の切り分けを変える際に新しい Permission を増やすだけで済む。 - 初版では
CLIENTS_ROLES_WRITEの付与先をCLIENTS_WRITEと同じ(PLATFORM_ADMINのみ)にする——ADR 0005 の「狭く始めて実運用で広げる」方針を踏襲。CLIENTS_ROLES_WRITE_OWN_ORGは ADR 0006 改訂(issue #166, org-client-admin)の仕組みにそのまま乗せる(ownerOrganizationIdが呼び出し元自身の Organization である Client に対してのみ)。
- 却下した代替案: 既存の
protected-client-ids(identity-console等)は本機能の対象外。保護対象 Client への適用は Client 自体の編集が保護されているのと同じ理由で拒否する(403 ではなく、Client 編集と同じ「対象として存在しない」404 相当)。
4. API 契約(identity-admin, /api/console/v1/clients/{clientId}/roles)
Section titled “4. API 契約(identity-admin, /api/console/v1/clients/{clientId}/roles)”ロールの定義は Client 起点、ユーザーへの付与は既存 console-roles と同じユーザー起点・丸ごと置換の形にする——車輪の再発明をしない。
| メソッド | パス | 権限 | 内容 |
|---|---|---|---|
GET |
/api/console/v1/clients/{clientId}/roles |
CLIENTS_READ(既存) |
ロール一覧(名前・description) |
POST |
/api/console/v1/clients/{clientId}/roles |
CLIENTS_ROLES_WRITE(_OWN_ORG) |
ロール作成 {name, description?, reason} + Idempotency-Key |
DELETE |
/api/console/v1/clients/{clientId}/roles/{roleName} |
同上 | ロール削除 {reason} |
GET |
/api/console/v1/users/{userId}/clients/{clientId}/roles |
USERS_READ(既存) |
そのユーザーが持つ、この Client のロール集合 |
PUT |
/api/console/v1/users/{userId}/clients/{clientId}/roles |
同上 CLIENTS_ROLES_WRITE(_OWN_ORG) |
ロール集合を丸ごと置換 {roles: [...], reason} + Idempotency-Key(ConsoleRolesController.replace と同じ形) |
- 全ての書き込みは既存の
OperationService.execute(actor, request) { ... }パターンに乗せる——reason必須、Idempotency-Key必須、TargetTypeにCLIENT_ROLE(または既存CLIENTを流用しロール名を detail に含める、実装 PR で確定)を追加。 - ロール削除は、そのロールを現在持っているユーザーがいても実行できる(Keycloak 側が role mapping ごと削除する)。削除前に件数を警告表示するかは Console UI 側の UX 判断(本 ADR は API 契約のみ決める)。
5. Console UI
Section titled “5. Console UI”ClientDetailPage.jsxに新しい「ロール」タブを追加: ロール一覧 + 作成・削除。- 各ロール行に「ユーザーを管理」→ ユーザー検索(既存の
console/src/features/users/の検索コンポーネントを再利用)+ 付与・剥奪ダイアログ——見た目は issue #167 のconsolerolesUI を踏襲するが、バックエンド呼び出し先はclientIdパラメータ化された新 API。 identity-console自身(protected)はこのタブを表示しない(CLIENTS_READのレスポンスに「ロール管理不可」フラグを含めるか、フロント側でprotectedClientIds相当の判定をするかは実装 PR で決める)。
6. アプリケーション側のガイダンス(docs/integration.md 拡充)
Section titled “6. アプリケーション側のガイダンス(docs/integration.md 拡充)”以下を追記する(新しい API は不要、ドキュメントのみの追加):
- access token をデコードして
resource_access[clientId].rolesを読む具体例(既存の「参考: 権限情報の確認方法」セクションに实装例として追記)。 - ロール変更の反映タイミング: Console でロールを付け外ししても、そのユーザーが既に持っている access token にはすぐには反映されない(token が自然に失効し、再ログインまたはリフレッシュで新しい token が発行されるまで古いロールのまま)。即時に反映したい場合は Console から該当ユーザーのセッションを明示的に取り消す(ADR 0008 の範囲外、既存の session revoke 機能と組み合わせる運用)。これは back-channel logout の “token は失効まで有効” という既存の注意書き(
identity-admin/README.md)と同じ性質のトレードオフである。 - 粗粒度の role-based access だけでは足りない場合(レコード単位の権限等)は、このプラットフォームは Keycloak Authorization Services(UMA 2.0)を一切使っておらず現状サポート外である旨を明記する——アプリ側は自前のレコード単位認可を実装するか、別途プラットフォーム側に要望を出す。
7. 既存 consoleroles(issue #167)との関係
Section titled “7. 既存 consoleroles(issue #167)との関係”- 置き換えない。
identity-consoleはprotected-client-idsのままで、consoleroles/ADMIN_USERS_MANAGEは現状維持する。本機能の対象はそれ以外の自助サービス Client のみ。 - 理由:
identity-consoleの5ロールは固定の意味(platform-adminが全権限を持つ等)を Console 自身のコードが前提にしており、汎用ロール管理に混ぜると「Console 自身の権限モデル」と「他アプリの任意ロール」が同じ経路で書き換えられるようになり、ADR 0006 の保護対象 Client 原則と矛盾する。 - 両機能の重複(ユーザー起点・丸ごと置換という契約形状が同じ)は意図的——将来
consolerolesをこの汎用機能の特殊ケースとして統合するかどうかは、必要性が出た時点で別 ADR として検討する。
Non-Goals
Section titled “Non-Goals”- Organization Groups 経由の一括付与: 部署単位でロールを一括付与する機能は初版スコープ外。まずユーザー個別付与のみ提供し、必要になった時点で別 ADR とする。
- Keycloak Authorization Services(UMA 2.0): 使わない。このプラットフォームのどの Client・ADR にも前例がなく、ADR 0006 も「Keycloak Fine-Grained Admin Permissions の調査」を別タスクとして先送りしている。
- SAML Client のロール管理: ADR 0006 と同じ理由で後続フェーズ。
- 既存の本番 Client(
identity-console/identity-console-service/claude/account-console等)への遡及適用: 対象は新規作成された自助サービス Client のみ。 - ロール名の事後変更(rename): 作成時に決まり、削除して作り直す以外の変更経路は持たない(Client の
clientIdと同じ「作成時に決まり、以後変更不可」パターン)。
Consequences
Section titled “Consequences”- 連携アプリは権限そのものの設計・保存・管理 UI を一切持たなくてよくなり、access token の claim を読むだけで権限判定ができるようになる——典型的な「粗粒度ロール(閲覧者/管理者程度)で足りるアプリ」であれば、このプラットフォームに乗るだけで権限管理が完結する。
- 一方で、Console 側の書き込み経路が1つ増える(
CLIENTS_ROLES_WRITE系)ことで、監査ログ・権限マトリクスの複雑さも増える。実装 PR ではOperationAction/TargetTypeの具体的な追加値と、ClientScopeService(issue #166)へのCLIENTS_ROLES_WRITE_OWN_ORGの組み込み方を確定する必要がある。 - ロール変更の反映が token の自然失効待ちになる非即時性は、アプリ側のドキュメントで明示しないと「権限を剥奪したのに使えてしまう」という問い合わせを招きやすい——back-channel logout の既存の注意書きと合わせて、連携ガイド側の周知が実装と同じくらい重要になる。
🤖 Generated with Claude Code