はじめに
本記事では、Keycloakと近年注目を集めている新しいアクセス制御方式であるReBAC (Relationship-based Access Control) の実装の1つである、OpenFGAを連携させてアプリケーションのアクセス制御を行う例を紹介します。
本記事で紹介する方法は、まだPoCレベルのものです。
ReBAC (Relationship-Based Access Control) とは
ReBACとは、リソースへのアクセス権限がサブジェクトとリソースの間の関係性によって定義されるアクセス制御方式です。Wikipediaの説明によると、ReBACという用語自体は2006年と随分前に登場しているようですが、ここ数年でたくさん聞くようになりました。そのきっかけといえば、2019年にGoogleより公開された論文「Zanzibar: Google’s Consistent, Global Authorization System」でしょう。Googleのサービス群を支える巨大な認可システムであるZanzibarでは、このReBACによるアクセス制御方式を実現したものでした。そしてこの論文からインスパイアを受けて、今回紹介するOpenFGAを含めたいくつかのOSSやプロダクト、サービスが誕生しています。
OpenFGAとは
OSSのReBAC実装のひとつで、Auth0が提供するAuthorization as a ServiceであるAuth0 Fine Grained Authorization (FGA) のコア部分がOpenFGAとして公開されて誕生しました。その後、CNCFのSandboxプロジェクトとして承認され、クラウドネイティブなReBAC実装として開発が進められています。
- GitHub: https://github.com/openfga/openfga
- ライセンス: Apache-2.0 license
OIDC / OAuth 2との関係
OpenFGAなどのReBAC実装が提供する範囲は、OIDCやOAuth 2のスコープ外の領域です。OIDCはOPからRPに認証連携するところまでが担当範囲であり、アプリケーション側でのアクセス制御をどのように実現するかはノータッチです。一方、OAuth 2によるAPI認可では、アクセストークンをOAuthクライアントに渡し、リソースサーバのAPI呼び出しでアクセス制御に利用するため、関連があるように思えるかもしれません。しかしながらOAuth 2では、あくまでOAuthクライアントに対してAPIアクセス権を委譲するためのものであり、最終的にエンドユーザがアプリケーションを利用する際のきめ細かいアクセス制御に利用するのは推奨されません。このあたりの議論は以下の記事によくまとまっていますので、参照されるとよいでしょう。
一方で、2023年10月にOpenID FoundationにてAuthZENというWorking Groupが設立されましたので、これについて少し触れておきます。昨今、ReBAC実装も含めてですが認可をアプリケーションから切り離して外部化するプロダクトやサービスが数多く登場しています。OpenFGAと同じくCNCFのOpen Policy Agent (OPA) も、ReBAC方式ではありませんがそのひとつです。この分野の標準化は、20年前以上にXMLベースのプロトコルとしてXACMLが存在しましたが、SAMLやOIDC、OAuthと比べると普及は限定的でした。このWorking Groupでは、ユースケースや認可方式の文書化、PEP-PDP間プロトコルの標準化に取り組んでいくとのことです。将来、OpenFGAを含めた様々なプロダクト間で相互接続性が高まるかもしれません。今後の動向に注目しています。
KeycloakとOpenFGAの連携
KeycloakとOpenFGAを連携させるPoCプロジェクトとして、Martin Besozzi氏によるKeycloak integration with OpenFGA (based on Zanzibar) for Fine-Grained Authorization at Scale (ReBAC) が公開されていますので、今回はこれを試してみます。下図は、このサンプルのアーキテクチャ図です (出所:https://github.com/embesozzi/keycloak-openfga-workshop )。KeycloakとOpenFGA、そしてサンプルアプリケーションが含まれています。
サンプルアプリケーションは、Eコマースサイトの商品を管理するアプリケーションを模したものです。アプリにログインすると商品一覧を参照できます。商品は公開中かどうかを示すステータスを持っており、未公開の商品については公開するための更新オペレーションを実行できます。以下、商品一覧を参照する際の処理シーケンスになります (図中の番号と対応しています)。
- ユーザは「App」にアクセスし、OIDCでKeycloakでログインして認証連携します。
- ユーザが「App」の画面で「View product」というアクションを行うと、「App」はそのバックエンドである「API Products」に対して、OIDCで取得したアクセストークンを付与してREST APIを呼び出します。
- 「API Products」はアクセストークンの検証に加えて、アクセストークンに紐づいているユーザ (つまりログインしたユーザ) が「View product」の権限を持っているかどうかを、OpenFGAに問い合わせて確認します。
このサンプルではKeycloakでユーザとレルムロールを管理し、そしてユーザとレルムロール、レルムロールとレルムロールのアサイン関係をKeycloak上で結ぶタイミングで、その情報をOpenFGAに登録するアーキテクチャになっています。レルムロールでは商品の参照権限を表すview-product
、更新権限を表すedit-product
と、それらのレルムロールを含むビジネスロールとしてadmin-catalog
、analyst-catalog
の合計4つのレルムロールを定義しています。KeycloakではComposit Roleという、ロールの中にロールを含める機能がありますので、admin-catalog
はview-product
とedit-product
の両方を含み、analyst-catalog
はview-product
のみを含むように設定されています。これにより、analyst-catalog
をアサインしたユーザに関してはview-product
権限しかなく更新処理は実施できない、ということを表現しようとしています。
KeycloakからOpenFGAへの上記関係データの登録は、「Event Publisher」という形で連携するアーキテクチャになっています。KeycloakのアドオンとしてEventListener SPIを実装したKeycloak OpenFGA Event Publisher が組み込まれており、このアドオンはKeycloakのレルムロールのアサイン処理のイベントをフックして、その内容をOpenFGAにプッシュするようになっています。
現状このアドオンは、障害ケースにうまく対応できないのが気になるところです。KeycloakからOpenFGAへのプッシュが何らかの理由で失敗した場合に、それをリトライしたり、別途同期するような仕組みが現状はありません。
さっそく動かしてみる
GitとDocker (Docker Compose) が利用できる環境であれば、すぐに試すことができます。
# git clone
git clone https://github.com/embesozzi/keycloak-openfga-workshop
cd keycloak-openfga-workshop
# 起動
docker-compose -f docker-compose.yml -f docker-compose-apps.yml -f docker-compose-openfga.yml -f docker-compose-import.yml up
また、ブラウザでアクセスする端末のhosts
ファイルに以下を追加しておきます。
127.0.0.1 keycloak openfga store store-api
動作テスト: 参照権限があるユーザでアクセス
-
「App」のURL http://store:9090/ にアクセスします。右上の「LOG IN」をクリックします。
-
適当な商品の
PUBLISH
というリンクをクリックします。すると、以下のように権限のない旨のエラーが表示されます。
動作テスト: 更新権限があるユーザでアクセス
一度ログアウトし、今度はrichard
でログインします (パスワードは同じくdemo1234!
)。先程と同じようにPUBLISH
リンクをクリックすると、今度は成功メッセージが表示されます。
※注意:サンプルアプリで実際に商品ステータスを更新する処理は実装されていないため、リンクをクリックしてもPUBLISHED
にはなりません。
動作テスト: 参照権限もないユーザでアクセス
同様に、今度はpeter
でログインします (パスワードは同じくdemo1234!
)。すると、このユーザは参照権限すら持たないため、商品を何も参照することができません。
OpenFGAを覗いてみる
http://localhost:3000/playground をブラウザで開くと、OpenFGAが提供するプレイグラウンド機能が利用できます。ここでは、今回のサンプルアプリのReBACモデルの確認と評価を、画面から確認することができます。
このサンプルでは、以下のReBACモデル定義がされています。ReBACではこのように、アプリケーションのモデル定義を行い、その定義したリレーションに基づいてアクセス制御の判断 (認可判断) を自動的に行います。なお、この定義は専用のDSLを利用することが多いですが、残念ながら標準的なものは存在しないため、現状ではReBACの実装ごとにそれぞれの文法を覚える必要があります。
model
schema 1.1
type group
relations
define assignee: [user]
type role
relations
define assignee: [user] or assignee from parent or assignee from parent_group
define parent: [role]
define parent_group: [group]
type user
サンプルでは、user
、role
、group
という3つのオブジェクトタイプを定義していますが、アプリケーション内で現状使われているのはuser
とrole
のみです。ポイントは、role
のリレーション定義から抜粋した以下の部分です。
define assignee: [user] or assignee from parent
define parent: [role]
- 1行目:
assignee
というリレーションで、user
タイプの要素と関係性を持っています。つまり、ロールはユーザにassignee
というリレーションでアサインされることを示しています。また、OR条件でassignee from parent
と書いてあります。OpenFGAでは、間接的なリレーションをX from Y
という表記で表すことができます。これにより、parent
リレーションで結ばれた親ロールからassignee
リレーションで関係性のあるオブジェクトに対しても間接的にリレーションがあることを示しています。 - 2行目:
parent
というリレーションで、role
タイプと関係性を持っています。つまり、ロールは親ロールを持てる構成になっていることを示しています。
モデルの定義に加えて、画面左下にはOpenFGAに登録されたタプル (定義したモデルに対する実際の関係データの組み合わせ) の一覧を参照することができます。これらのタプルは、前述で紹介したKeycloak OpenFGA Event Publisherにより、KeycloakからOpenFGAにプッシュされたデータ群です。サンプルでは最初から以下のように5つのタプルが登録されています。
これらの関係性を図示すると、以下のようになります。
このサンプルのモデル定義は、OpenFGAや他のReBAC実装のサンプルモデルと比べると、違和感を感じるかもしれません。というのも、このサンプルプロジェクトはKeycloakのユーザとレルムロール (とグループ) の関係性をタブルとしてOpenFGAに登録する都合上、その関係性をそのままReBACのモデルとして実装しているためです。
よりReBACらしいサンプルモデルとしては、OpenFGAのModeling Guidesを一読するとよいでしょう。Google DriveやGitHub、Slackという馴染みのあるサービスを例にモデル定義例も紹介されています。
このように、view-product
とedit-product
という権限を表す末端ロールはユーザに直接関連付けられてはいません。このような関係性であっても、ユーザが間接的にview-product
やedit-product
ロールを持っているかどうかをリレーションのグラフを辿って自動的に評価してくれます。この点がReBACのメリットのひとつです。
アプリケーションのアクセス制御の実装
最後に、アプリケーション側で実施しているOpenFGAを利用したアクセス制御箇所についても見ておきましょう。バックエンドである「API Products」は、Node.jsでExpressを利用して実装されています。OpenFGAではNode.js用のSDK Clientを提供しているため、これを利用したMiddlewareを実装して組み込み、API呼び出し時にアクセス制御を実装しています。具体的には、OpenFGAのRelationship Queries (Check API) を利用して権限があるか (リレーションを持っているか) をOpenFGAに問い合わせ、結果として true
or false
を受け取っています。
const checkTuple = async function (user, relation, object) {
console.log(`[Store API] Check tuple (user: '${user}', rel: '${relation}', obj: '${object}')`);
try {
let client = await getClient();
let { allowed } = await client.check({
tuple_key: {
user: user,
relation: relation,
object: object
}
});
console.log(`[Store API] Check tuple for user: ${user} isAllowed: ${allowed}`);
return allowed;
} catch ( e ) {
console.log(e);
return false;
}
}
作成されたMiddlewareをRouterにて以下のように組み込み、使用しています。商品の公開操作は、ログインユーザにedit-product
のロールがリレーションを辿って存在するかチェックしています。
router.route('/:id/publish')
.post(
[
jwt.validateToken,
jwt.decodeToken,
fga.checkUserHasRole("edit-product")
],
productsController.publish);
まとめ
KeycloakとReBACのOSS実装であるOpenFGAとの連携について、PoCプロジェクトを一例に紹介しました。このサンプルではKeycloakのロールモデルをそのままReBACモデル化しているため、あまりReBACらしさを感じないサンプルにはなってしまっているのが少々残念なところです。ReBACに興味を持たれた方は、是非公式ドキュメントを参照してみてください。ReBACはアプリケーションによってははまると強力なツールになる可能性を秘めているのではないか、と期待していますので、引き続きウォッチしていきたいと思います。