ADR 0003: 会社境界は Keycloak Organizations、関電工内の組織図はネイティブ Organization Groups でモデル化し、どちらも人事マスターデータを正とする
- Status: Proposed
- Date: 2026-10-07(2026-10-07 改訂1: レビューで指摘された4点——Organization 所属の一意性保証、認証方式の誤った推論、メンバーのライフサイクル未定義、Groups モデル未確定——を反映。2026-10-07 改訂2: サービスアカウントの
view-organizationsロール要件を確認して反映、§5 の「確認済み federated identity」だけでは不十分という指摘を反映、実装(PR #53)側の未知パラメータ拒否漏れを修正。2026-10-07 改訂3: 実機(Keycloak 26.7.5、ローカル dev realm)で Organizations の有効化方法と managed/unmanaged メンバー表現を実際に確認——Consequences 参照)
Context
Section titled “Context”プラットフォームの4コンポーネント構成(MyPage/管理コンソール/Identity Hub 業務ロジック層/Keycloak)は ADR 0001・0002 で確定済み。管理コンソールは関電工の社内IT管理者と、協力会社側の代表者の両方が使う(platform_architecture メモリ参照)。
今回の論点は、まだ未設計のまま放置されている次の2点:
- 関電工本体の組織図(部署階層)をどこから取り込み、誰が管理するか。
- 関電工本体のユーザーと、複数ある協力会社それぞれのユーザーを、プラットフォーム上でどう区別し、権限の境界をどう引くか(協力会社の代表者は自社ユーザーだけを管理できる、など)。
調査した現状:
device-reputation/docs/05-session-management-design.mdには「Keycloak 自体に『組織』という概念はなく、本リポジトリにもまだ存在しない(SCIM の Epic #9 以降に登場する)」という記述があるが、これは古い。keycloak/Dockerfileが固定している Keycloak 26.7.5 には、まさに今回のユースケース(1つの realm に複数の外部企業のユーザーが同居し、企業ごとに委譲管理者を置く)のために作られた、ネイティブな Organizations 機能がある。docs/superpowers/specs/2026-10-05-admin-console-design.mdでは、Organizations(Epic #8)・Groups(Epic #7)はナビゲーションのロードマップにだけ名前が残っていて、「管理範囲・関連元・最小権限を定義する」こと自体が未着手として明記されている。P0 スコープからは明示的に除外。- ADR 0001 はこの2つの integration 方向をすでに決めている: 「マスターデータ → Identity(人員・組織・協力会社)」は定期/イベント駆動の取り込みで良く、同期呼び出しを待たない。つまり組織構造データが人事マスターから流れてくること自体はすでに前提になっているが、Keycloak 側でそれをどう受け止めるかは未決定だった。
console/、identity-admin(main 未マージ)のどちらにも、組織・会社・部署に関するコードや UI は一切存在しない。
Decision
Section titled “Decision”会社の境界(関電工本体 vs 各協力会社)と、関電工内部の組織図(部署階層)は、別々の Keycloak プリミティブでモデル化する。
1. 会社境界 = Keycloak Organizations
Section titled “1. 会社境界 = Keycloak Organizations”- 法人格(関電工本体を含む)ごとに1つの Organization を作る。関電工自身も特別扱いせず1つの Organization として扱うことで、「ユーザーは必ずちょうど1つの Organization に属する」という一貫したモデルにする。
- この「ちょうど1つ」は Keycloak が保証する制約ではない。 Keycloak は1ユーザーが複数 Organization のメンバーになることを許す(公式ドキュメント)。したがって一意性は identity-admin 側のプロビジョニング経路でのみ強制する:
- 人事マスター同期ジョブは関電工 Organization のメンバーシップしか書かない(他の Organization への追加は一切行わない)。
- 協力会社代表者の招待フローは、招待対象ユーザーが既に別の Organization に属していないことを事前チェックし、既存メンバーシップがあれば拒否する。
- それでも複数 Organization 所属が発生した場合(手動操作・同期バグ・Keycloak 側の不整合など)、identity-admin の認可判定はこれを異常状態としてフェイルクローズで扱う(どちらの Organization 権限も与えず、運用アラートを出す)— 「複数ヒットしたら最初の1件を使う」のような曖昧な解決はしない。
- 協力会社代表者の管理者ロールは、ロール名だけでなく具体的な
orgIdに紐付けて付与する(今回の PR 範囲外の書き込み機能で実装)。ユーザーが転籍して別の Organization のメンバーになっても、古いorgIdに紐付いた権限は新しい所属に引き継がれない。
- この境界は将来、認証ソースの出し分けや Organization 単位の招待フローの土台になる(ただし「Organization 所属 = 認証方式の証明」ではない点は §3 で別途扱う)。
2. 関電工内の組織図(部署階層)= Keycloak Organization Groups(関電工の Organization 配下のみ)
Section titled “2. 関電工内の組織図(部署階層)= Keycloak Organization Groups(関電工の Organization 配下のみ)”- 部・課・係などの階層は、Keycloak 26.6 以降のネイティブ機能である Organization Groups(Organization に紐付いたネスト可能な Group 階層。公式アナウンス)で表現する。本リポジトリが固定する Keycloak バージョンは 26.7.5(
keycloak/Dockerfile)なので利用可能。- 代替案として検討した「
org_idカスタム属性付きの素の realm Group」は採用しない: Organization Groups の方が会社境界と部署階層の親子関係をそのまま表現でき、カスタム属性による紐付けの運用ミス(属性の付け忘れ・値の不整合)が構造的に起こらない。 - Organization Groups は Keycloak 自体の group-based authorization policies では使えないという制限があるが(公式アナウンスの比較表)、これは本 ADR の決定4(認可判定は Keycloak の認可プリミティブではなく identity-admin の業務ロジック層で行う)と完全に整合しており、採用の妨げにならない。
- P0 では関電工の Organization にのみ適用し、協力会社側は自社の内部階層を持たない(フラットなメンバーシップのみ)。必要になった時点で個別に拡張する。
- 代替案として検討した「
- 会社境界(Organizations)と社内階層(Organization Groups)はあくまで別概念として扱う。混在させると、将来ある協力会社に部署階層を持たせたくなったときに権限ロジックを引き直す必要が出てしまう。
3. データの正 (source of truth) とメンバーのライフサイクル
Section titled “3. データの正 (source of truth) とメンバーのライフサイクル”- 関電工の組織図とメンバー構成は人事マスターデータが正。Keycloak 側は同期先であり、Console UI は関電工の組織図に対して P0 では読み取り専用にする。書き込みは ADR 0001 ですでに決定済みの「マスターデータ → Identity」の独立同期ジョブ(BFF を経由せず業務ロジック層を直接呼ぶ)が、Keycloak Admin API 経由で Organizations/Organization Groups を upsert する唯一の書き手になる。
- 協力会社側には上流の人事システムが存在しないため、協力会社の Organization とその初期メンバーは関電工のIT管理者が Console から作成し、協力会社の代表者を招待する。招待された代表者は自社 Organization にスコープされた管理者ロールを得て、以後は自社ユーザーをセルフサービスで管理する。
- メンバーの種別(managed / unmanaged)を明示的に決める。 Keycloak は Organization メンバーを managed(Organization 経由で作成され、Organization からの除外・Organization 自体の無効化にアカウントのライフサイクルが連動する)と unmanaged(既存アカウントを Organization に関連付けただけで、Organization 側の操作とは独立にアカウントが存続する)に区別する(公式ドキュメント)。この区別を省略すると、契約終了後も unmanaged メンバーが realm にログインし続けられる、という事故につながりうる。
- 協力会社のメンバーは managed として作成する。 契約終了時に Organization を無効化する(または個別メンバーを除外する)運用操作だけでアカウントのログインも止まる、という今回のユースケースの中心的な要件に合致する。
- 関電工社員は unmanaged として Organization に関連付ける。 人事マスターから同期される既存アカウントであり、そもそも Organization 経由で作られるわけではないため。関電工 Organization 自体を無効化する運用は想定しない。
- 離職・契約終了時の具体的な処理(協力会社 Organization の無効化手順、個別メンバーの除外、代表者の管理者ロール失効のタイミング)は書き込み機能の実装時に定義する — 本 PR の読み取り専用スライスには含まれない。代表者の管理者権限は、Organization メンバーシップを失った時点で identity-admin 側のライブ確認(既存の
ConsolePrincipal検証と同じ仕組み)により失効させる。既存の freshness ポリシー(読み取りキャッシュ最大60秒)と同じ理由で、失効まで数十秒のラグが生じることは許容する。 - 有効な会社帰属(ちょうど1つの Organization への所属)を持たないユーザーは、identity-admin のどの管理操作からも対象として扱わない(フェイルクローズ)。
4. 権限の委譲ポイントは identity-admin の業務ロジック層
Section titled “4. 権限の委譲ポイントは identity-admin の業務ロジック層”- ADR 0001 の「権限チェック・ビジネスルールは application service 層に置く」という方針を踏襲し、Keycloak の Organization 自体の管理者権限プリミティブではなく、identity-admin の業務ロジック層が実際の認可判定を行う。Keycloak Organizations はあくまでメンバーシップのデータモデルとして使う。
- 実効スコープ: 関電工内部管理者ロール = 全 Organization、協力会社代表者ロール = 自分の Organization のみ(§1 の通り、具体的な
orgIdに紐付けて付与する)。
5. Organization 所属から認証方式(パスワード/MFA の管理元)を直接推論しない — ただし「確認済み federated identity」だけでも不十分
Section titled “5. Organization 所属から認証方式(パスワード/MFA の管理元)を直接推論しない — ただし「確認済み federated identity」だけでも不十分”- 当初案は「関電工の Organization ⇒ Entra federation、それ以外 ⇒ ローカル認証」と Organization 所属だけで認証方式を決める想定だったが、これは誤り。Keycloak では Organization のメンバーであっても独立してローカル認証情報を持つことを許し、Organization 所属そのものは「その人が今どうやってログインしているか」の証明にはならない。
- 2026-10-07 さらに改訂: 最初の修正では
UsersService.sourceOf()が使う「確認済み federated identity の有無」(GET /users/{id}/federated-identity)を代わりの判定根拠にしたが、これもまだ不十分という指摘を受けた。Keycloak では brokered identity の連携とローカルのパスワード/MFA 資格情報は互いを排他しない — 管理者がブラウザ認証フローで明示的に「IdP 経由のみ」を強制しない限り、ユーザーは Entra 連携済みのままローカルパスワードも使える状態になりうる。つまりfederated-identityが非空であることは「連携が存在する」ことの証明にしかならず、「ローカル資格情報が存在しない/使われていない」ことの証明にはならない。これを根拠にパスワード/MFA 自己管理 UI を隠すと、実際に使っているローカル資格情報を管理する入口をユーザーから奪いかねない。 - したがって MyPage の UI 出し分けは、
sourceOf()の分類をそのまま再利用するのではなく、次のどちらか(または両方)を根拠にする必要がある — どちらも未確定で、MyPage 自体の設計(P1、未着手)で決定する:- 実際のローカル資格情報の存在を直接確認する。 例えば
GET /users/{id}/credentialsでパスワード/OTP 等の credential type が実在するかをサーバー側で確認し、存在する資格情報の種類に応じて UI を出す(「確認済み federated identity」と同じ『推測せず確認する』原則を、推論の矛先を変えて適用する)。 - 認証ポリシーとして明示的に強制する。 関電工社員はブラウザ認証フローでローカルパスワードを使えないよう構成し(IdP redirect 強制、または対象ユーザーのパスワード資格情報を作成しない運用)、その強制が存在する前提で UI を固定する。この場合も「強制されている」という事実はサーバー側の設定として確認可能でなければならず、Organization 所属からの推測であってはならない。
- 実際のローカル資格情報の存在を直接確認する。 例えば
- いずれの方式を採るにせよ、確定するまでは「関電工 Organization に属し、かつ brokered identity も確認できるが、ローカル資格情報も同時に有効」というユーザーが存在しうる前提で設計する。
- 2026-10-14、ADR 0007 で方針決定(完全な解消ではない): 上記2案のうち (2) を採用する形で決着した。browser flow のパスワード分岐に、Entra 連携済みロールを持つユーザーを拒否する条件を追加し(
formsサブフローと IdP リダイレクト/ブローカー経路が相互排他である Keycloak の構造を利用)、「関電工社員はローカルパスワードでログインできない」ことをブラウザ認証フローの設定として強制する。この強制自体が realm の認証フロー設定として確認可能であり、Organization 所属からの推測ではない——ここまでは解消。ただし強制の根拠は「Entra 連携済みロールを持つこと」であって「社員であること」ではない。 ロールは実際に Entra 経由でログインしたユーザーにしか付かないため、一度も Entra でログインしていない社員のローカルパスワードは、別途のレポート・ロール補完ツール(ADR 0007 §7)で検出・補完されるまで有効なまま残りうる。したがって本項目の未決事項(「連携関連付けはローカル資格情報が存在しないことを証明しない」)は、ADR 0007 とそのロールアウト順序(§6)が本番適用されるまでの間、また適用後もツール実行前にログインした社員については、引き続き成り立つ前提として扱う。
Consequences
Section titled “Consequences”- Epic #8(Organizations)・#7(Groups)は「名前だけで未設計」の状態を脱し、具体的な形が決まる: Organizations = 会社境界 + 協力会社オンボーディング(managed メンバー)+ 一意性のフェイルクローズ、Organization Groups = 関電工限定の人事マスター由来の部署階層。
- サービスアカウントに必要なロールは確認済み: Keycloak 26.7.0 で追加された realm-management の専用ロール
view-organizations(manage-realmのような高権限ロール不要)。scripts/dev-setup-console.shに反映済み、ローカル dev realm(Keycloak 26.7.5、make kc-up dev-realm+make console-up)に対して実際に BFF 経由でGET /organizations・GET /organizations/{id}・GET /organizations/{id}/membersを呼び、Console の Organizations 一覧・詳細・メンバータブが実データで描画されることを確認した(テスト組織 “Kandenko” + メンバー1名、console-viewerでログイン)。 - 実機で確認(2026-10-07 改訂3): Organizations はビルド時のフラグではなく realm 単位の設定。
organizationsEnabledという realm representation のブール値で、デフォルトはfalse。無効なままだとGET /admin/realms/{realm}/organizationsが404 {"errorMessage":"Organizations not enabled for this realm."}を返す——keycloak/Dockerfileの--features=organizationのようなビルド引数は不要。scripts/dev-setup-realm.shの gcu realm 作成/更新 JSON に"organizationsEnabled":trueを追加済み。本番/他環境の realm では、組織機能をロールアウトする前にこのフラグを個別に有効化する運用が要る(IaC/手動どちらであれ、見落としやすい1行なので要注意)。 - 実機で確認: メンバー取得レスポンスには
membershipType(MANAGED/UNMANAGED)が含まれる。 §3 の managed/unmanaged 区別は Keycloak Admin REST API のレスポンスに実際に出現するフィールドで確認済み(Keycloak の Admin REST API 経由で追加したメンバーはUNMANAGEDとして返る)。読み取り専用のKeycloakUserRepresentation(identity-admin)はこのフィールドを無視して問題ない(@JsonIgnoreProperties(ignoreUnknown = true))が、将来この値を Console 側で表示・利用する場合はフィールドを追加する必要がある。 - 組織とドメインの wire 表現(
{id, name, alias, enabled, domains: [{name, verified}]})も実機で確認済み——identity-adminのKeycloakOrganizationRepresentationの想定と一致した。 - 実装前に、固定している Keycloak 26.7.5 に対して次の点はなお検証する必要がある(本セッションでは未検証):
- managed/unmanaged メンバーの作成・除外 API の正確な挙動と、Organization 無効化がログインを実際に止めるかどうか(今回確認したのは Admin REST 経由の読み取りのみで、書き込み・ライフサイクル操作はこの PR の範囲外)。
- 複数 Organization 所属の検知方法(§1 のフェイルクローズを実装する前提)。
- Epic #7/#8 着手前にこれらのスパイクを先に行う。Organization Groups の採否自体(ネイティブ機能を使う)は本 ADR で決定済みで、再検討の対象ではない。
- Console の P0(ユーザー一覧/詳細)は本 ADR だけでは変更不要。ただし P1 ロードマップにすでに予約されている Organizations/Groups 画面は、このモデルを前提に設計する。
- 協力会社のオンボーディングという、これまで未定義だったフローが明文化される(関電工管理者が Organization 作成 → 代表者招待 → 代表者が自社スコープの管理者ロールでセルフサービス)。契約終了時の Organization 無効化・メンバー除外・代表者権限失効(§3)も含めて、P0 実装計画のタスク一覧にはまだ無く、Epic #8 着手時に個別タスク化が必要。
- MyPage(ADR 0002)のパスワード/MFA 自己管理 UI 出し分けは、本 ADR では解決しない未決事項として持ち越す(§5)。「Organization を見れば分岐が決まる」という当初想定も、その次の「確認済み federated identity があれば決まる」という案も撤回済み。2026-10-14、ADR 0007 の MyPage 決定で一時的に先送り: Entra 連携ユーザー向けの MFA 案内カードは全ユーザーに無条件で表示し、出し分けロジック自体は実装しない(バックエンド変更なし)。本項目が求めていた「実際のローカル資格情報の確認、または認証ポリシーの明示的な強制のどちらを採るか」の決定は、MyPage 第2フェーズの能力モデル確定(#64、未着手)まで持ち越されたままである。
- main には
identity-admin/console/がまだマージされていないため、本 ADR によって既存のマージ済みコードに影響はない。
Alternatives considered
Section titled “Alternatives considered”- Groups のみで全体をモデル化する(Organizations を使わない): プリミティブが1つで済むが、Keycloak ネイティブのメンバー招待・将来の Organization 単位 IdP 紐付けを捨てることになり、委譲境界をすべて自前の
companyカスタム属性で作り込む必要がある。まさにこのユースケースのために用意された機能を使わない選択になる。- 2026-10-15 追記(issue #181 の調査、keycloak/README.md 参照): 「将来の Organization 単位 IdP 紐付け」を実際に有効化する際の未着手項目を具体化しておく——
gcuテーマ用のlogin-username.ftl(Identity-First のユーザー名のみ1ページ目、現状未実装)を用意すること、および、そのリダイレクト先 IdP のポストログインフローでの第二要素(MFA)の扱いを決定すること。後者は ADR 0007 の分担(Entra 連携社員の MFA は Entra が担う)を前提にすると、Keycloak 側でこの経路に TOTP 強制を追加しない、という既定の選択にはなるが、実際に有効化する前に明示的な決定として確定させる必要がある。
- 2026-10-15 追記(issue #181 の調査、keycloak/README.md 参照): 「将来の Organization 単位 IdP 紐付け」を実際に有効化する際の未着手項目を具体化しておく——
- Organizations のみで全体をモデル化する(部署を Organization の属性や Role で代替): 関電工の多階層組織図(部 > 課 > 係)を表現できず、部署ごとに Organization を作ると「法人格・請求境界」と「レポートライン」が混ざってしまう。協力会社という浅い階層と、関電工という深い階層の非対称性にも合わない。