プログラミング
信頼性が高く(そして高速な)ディレクトリ同期の構築
Building reliable (and fast) directory sync (firezone.dev)
要約
この記事では、IDプロバイダーからアプリケーションへユーザーやグループ情報を同期する「ディレクトリ同期」の複雑さについて解説しています。SCIM標準の概要と、各IDプロバイダーの実装のばらつきによる課題を詳述し、Firezone社が独自エンジンの開発を選択した理由を説明しています。
全文翻訳
「あなたのユーザーは誰ですか?」— マスターコントロールプログラム、TRON (1982) ユーザーIDがアプリケーションにとって重要であるなら、それを管理する方法が必要です。通常、これはユーザーアカウントを意味し、誰かがサインアップしたときに作成されます。しかし、従業員が思いつくままにSaaSアプリにサインアップできるようにすることは、管理上の悪夢です。そのため、組織はアプリケーション内に存在する(または存在しない)ユーザーを制御する方法を求めています。それは誰がサインインできるかを制御しますが、何ができるかはどうでしょうか?そのためには、ユーザーと同様に、組織のIDプロバイダー(Entra、Google、Oktaなど)から提供されるロールまたはグループが必要です。グループはFirezoneのアクセスモデルの基盤を形成します。それらが誰が何にアクセスできるかを決定します。ユーザーとグループをアプリケーションに取り込むプロセスはディレクトリ同期と呼ばれ、それをうまく行うことは驚くほど難しいことです。この記事では、ディレクトリ同期とは何か、それを実装するための主要な標準、そしてなぜ当社がそれを完全に放棄して独自のエンジンをゼロから構築することを選択したのかを説明します。
ディレクトリ同期:簡単な入門組織のディレクトリとは、そこに誰がいて、何にアクセスできるかのリストです。それはIDプロバイダーにあり、3つの要素で構成されています。
ユーザー:組織内の人々。それぞれに名前、メールアドレス、アクティブまたは一時停止などのステータスがあります。
グループ:エンジニアリングやプロダクトのような、名前付きのユーザーコレクション。グループを使用すると、一度に多くの人にアクセス権を付与できます。
メンバー:誰が何に属しているか。グループのメンバーは、ユーザーまたは別のグループです。ユーザーのアクセス権は、直接的または他のグループを介して、そのユーザーが属するすべてのグループから得られます。
ディレクトリ同期とは、そのディレクトリをアプリケーションにコピーし、最新の状態に保つプロセスです。IDプロバイダーが真実の源であるため、アプリケーションが保持しているのはコピーです。時間の経過とともに、そのコピーは3種類の変更を拾う必要があります:新しいユーザーとグループ、名前の変更のような更新、そして削除。誰かが参加、離脱、またはチームを変更したとき、理想的にはアプリケーションはすぐにそれを知る必要があります。
ディレクトリ同期が何でないかを明確にしておく価値があります:シングルサインオン(SSO)。これは明白に思えるかもしれませんが、多くの人が混同しています。OpenID Connectのような主要な認証標準は、ユーザーをアプリケーションにどのように取り込むかについては何も述べていません。ディレクトリ同期では、ユーザー(およびグループ)を取り込むメカニズムを指しており、認証方法ではありません(そのために、この記事を参照してください)。
簡単な例あなたの組織に以下のディレクトリがあるとします。
例:ネストされたグループを持つディレクトリ。2つのグループと2人のユーザーを持つディレクトリ。Productグループには、ユーザーAliceとグループEngineeringの2人のメンバーがいます。Engineeringグループには、ユーザーBobという1人のメンバーがいます。EngineeringがProductのメンバーであるため、Bobは間接的にProductに属しています。
グループ:Product
ユーザー:Alice
グループ:Engineering
ユーザー:Bob
あなたのデータベースでは、次のようなものになるかもしれません。
例:データベーステーブルとしてのディレクトリ。3つのテーブル。UsersはAliceとBobをリストします。GroupsはProductとEngineeringをリストします。Membersにはuser列、group列、parent_group列があります。AliceはProductのユーザーです。BobはEngineeringのユーザーです。グループEngineeringはProductにあります。各行は最初の2つの列のいずれかを空にし、BobがProductにいるかどうかを尋ねるには、親グループを再帰的なクエリでたどる必要があります。
Users
name
Alice
Bob
Groups
name
Product
Engineering
Members
user
group
parent_group
Alice
-
Product
Bob
-
Engineering
-
Engineering
Product
アプリケーションで「BobはProductのメンバーか?」のような一般的なアクセスに関する質問に答えるには、Productのメンバー、次にそこにある任意のグループのメンバーなどを調べて、Bobを見つけるまで調べる必要があります。これを避けるために、グループをフラット化し、各ユーザーに直接的または間接的に属するすべてのグループの行を提供できます。
例:フラット化されたメンバーシップを持つディレクトリ。3つのテーブル。UsersはAliceとBobをリストします。GroupsはProductとEngineeringをリストします。Membersにはuser列とgroup列があり、ユーザーが直接的または間接的に属するすべてのグループについて1行あります:AliceはProductに、BobはProductに、BobはEngineeringにいます。BobはEngineeringがProductにネストされているため、Productにいます。ネストされたグループは行として表示されなくなるため、BobがProductにいるかどうかを尋ねることは単一のルックアップになります。
Users
name
Alice
Bob
Groups
name
Product
Engineering
Members
user
group
Alice
Product
Bob
Product
Bob
Engineering
これで同じ質問は簡単なルックアップになります。さて、データモデルはこうです。次に、それをIDプロバイダーからデータベースに取得し、最新の状態に保つ必要があります。
SCIM
もちろん、これをすべて自分で発明する必要はありません。ディレクトリ同期を行うための標準があります。それはSCIMと呼ばれ、その説明は次のとおりです。
System for Cross-domain Identity Management (SCIM) 仕様は、標準化されたサービスを通じて、マルチドメインシナリオでのID管理をより簡単にサポートできるHTTPベースのプロトコルです。
実際には、アプリケーションに次のようなRESTエンドポイントのリストを作成することを意味します。
GET /Users
GET /Users/{id}
POST /Users
PUT /Users/{id}
PATCH /Users/{id}
DELETE /Users/{id}
GET /Groups
GET /Groups/{id}
POST /Groups
PUT /Groups/{id}
PATCH /Groups/{id}
DELETE /Groups/{id}
そして、IDプロバイダーがそれらを呼び出してディレクトリを同期させます。
SCIMの約束4つのIDプロバイダー(Okta、JumpCloud、Entra、Google)はすべて、アプリケーション内の同じSCIM APIにSCIMリクエストを送信し、単一のデータベースに書き込みます。SCIMの約束は、1つの標準APIがあれば、どのプロバイダーでも同期できるということです。
IDプロバイダー
あなたのアプリ
Okta
JumpCloud
Entra
Google
SCIM API
データベース
SCIMの約束:複数のプロバイダーを同期するための単一のAPI
SCIMの良い点は、IDプロバイダーがアプリケーションに変更をプッシュすることです。そのため、プロバイダーで更新が発生してからアプリケーションに着陸するまでの遅延時間は、スケジュールに基づいてプロバイダーをポーリングするプルベースのアプローチよりもはるかに短くなる可能性があります。
紙の上では素晴らしいように聞こえます。しかし、ここでの問題は何でしょうか?まず、アプリケーションはほぼ常に稼働しており、ディレクトリの更新を受け入れられる状態である必要があります。数秒間でもダウンしたり過負荷になったりすると、重要なディレクトリ更新を見逃す可能性があります。アプリケーションからフル同期をトリガーすることはできないため、IDプロバイダーが再度通知するまで、それらの更新は失われます。しかし、SCIMのより迷惑な点は、プロトコル(つまり、ワイヤーフォーマット)を標準化するのに優れている一方で、それらのエンドポイントがどのように呼び出されるべきか、プロバイダーでのライフサイクルイベントにマッピングされるか、あるいは各リクエストに含まれるデータの種類については何も述べていないことです。IDプロバイダーは、SCIMの実装方法がそれぞれ大きく異なります。以下に例を挙げます。
グループメンバーシップの更新:Oktaは追加と削除を1つのPATCHで同時に送信できますが、Entraはそれらを分割する必要があり、1回のPATCHにつきメンバーの削除は1つしか許可しません。同じプロバイダー内でも基本的な型が異なります:Entraは歴史的に、SCIMで定義されたブール値ではなく、アクティブ:「False」を文字列として送信し、後に準拠した動作に切り替えるための互換性フラグを追加しました。オプション機能は異なります:Oktaは、バルク操作、POST検索、/ServiceProviderConfig、およびmeta.lastModifiedでのフィルタリングなど、いくつかのSCIM機能を明示的に使用しません。ユーザーのデプロビジョニング、つまり退職した従業員のアクセスを遮断するという重要な操作になると、さらに多くの不整合が発生します。
Okta:順序が重要です:グループメンバーシップを削除する前にユーザーを解除し、それらのメンバーシップがダウンストリームアプリケーションに残る可能性があります。Entra:グループの削除は必ずしもユーザーを非アクティブにするわけではありません。他の割り当てられたグループが引き続きアクセスを許可している場合、スコープ内に残ります。JumpCloud:デプロビジョニングは、プロビジョニンググループからの削除だけでなく、アプリのバインド解除、一時停止、または削除に従うことができます。OneLogin:ユーザーの削除は、アプリのプロビジョニング設定に応じて、削除、一時停止、または何も引き起こさない可能性があります。
これらすべてが、期待していた一貫した実装の代わりに、多くの異なるプロバイダー固有のSCIMコードパスにつながるという現実につながります。IDプロバイダーごとに1つのSCIMシム。4つのIDプロバイダー、Okta、Jump