OpenFGAのモデリングガイドを読み解く(前編)

2024.03.21

OpenFGAは、ファイングレインド・アクセス・コントロール(Fine-Grained Access Control、FGAC)を実現するためのオープンソースの認可システムです。このシステムは、複雑なアクセス制御ポリシーを柔軟にモデリングし、アプリケーションやサービスに統合することを可能にします。

Fine-Grained Access Controlについてはこちらの記事がわかりやすかったので、参考にしてみてください。

さて今回の記事は、OpenFGA公式ドキュメントのモデリングガイドを読み解いていきます。

元のドキュメントは英語で書かれており日本語に翻訳してみたのですが、インラインコードまで翻訳してしまい、認可の構造を理解する上で重要な部分がわかりづらくなってしまいました。

そこで、この記事では英語の情報を日本語でシンプルにまとめつつ、参考コードを多めでお送りします。

では、はじめます。


モデリングのイントロダクション

一般的な権限チェックは

「ユーザーUは、オブジェクトOに対してアクションAを実行できますか」

ですがOpenFGAでは

「ユーザーUは、オブジェクトOと関係Rを持っていますか」

という考え方をします。

これをリレーションシップ・ベースド・アクセス・コントロール(Relationship-Based Access Control、ReBAC)と呼びます。

さて、公式ドキュメントではイントロダクションの次は以下のように続きます。

A Process For Defining Authorization Models

Defining an authorization model requires codifying an answer to the question "why could user U perform an action A on an object O?" for all use cases or actions in your system. This is an iterative process. For the purpose of this guide, we'll go through one iteration of this process using a simplified Google Drive like system as an example.

簡易的なGoogleドライブの認可を例に、モデリングをステップバイステップでやってみようということですが

雰囲気が掴めてないうちは理解が中々難しいので、完成版の簡易的なGoogleドライブの認可モデルと関係タプルだけ、さっと眺めるにとどめます。

簡易的なGoogleドライブの認可モデル(完成版)

model
  schema 1.1

type user

type organization
  relations
    define member: [user, organization#member]

type document
  relations
    define owner: [user, organization#member]
    define editor: [user, organization#member]
    define viewer: [user, organization#member]
    define parent: [folder]
    define can_share: owner or editor or owner from parent
    define can_view: viewer or editor or owner or viewer from parent or editor from parent or owner from parent
    define can_write: editor or owner or owner from parent
    define can_change_owner: owner

簡易的なGoogleドライブの関係タプル(完成版)

# アンは contoso 組織のメンバーです 
{ user:"user:anne", relation: "member", object: "organization:contoso"}

# ベスは fabrikam 組織のメンバーです    
{ user:"user:beth", relation: "member", object: "organization:fabrikam"}

# アンはドキュメント:1 を作成し、その所有者になります。
{ user:"user:anne", relation: "owner", object: "document:1"}

# アンは、編集者として fabrikam 組織のすべてのメンバーとドキュメント:1 を共有します。
{ user:"organization:fabrikam#member", relation: "editor", object: "document:1"}

# Beth が文書:2 を作成し、その所有者になります。
{ user:"user:beth", relation: "owner", object: "document:2"}

# Beth は、contoso 組織のすべてのメンバーと閲覧者として document:2 を共有します。
{ user:"organization:contoso#member", relation: "viewer", object: "document:2"}

それらしい単語が並んでいますが、最初はよくわからないと思います。

私も、このGoogleドライブのモデリングと完成版モデルを最初に見たとき、「直感的にわかるような気がするけれど、書き方に規則性を感じない…」と混乱しました。

例えば、モデルには owner と書いてある部分がありますが、同じ部分に can_share と書かれていたりします。一方はユーザーの性質を表す単語なのに、一方はユーザーの権限を表す単語です。

これは、オブジェクト同士の「関係」を定義する上での柔軟性だろうことは理解できましたが、逆に規則性は見えませんでした。

でも安心してください、本記事を最後までご覧いただければ理解できるようになると思います!

直接関係を定義する

まず、もっとも基礎的なアクセス付与から学びます。

例えば「ユーザー Jane はオブジェクト プロジェクト Sandcastle と関係 View を持つことができる」のモデルは以下のように表現できます。

model
  schema 1.1

type user

type project
  relations
    define can_view: [user]

このモデルの関係タプルはこのようになります。

# ユーザー `Jane` はオブジェクト `プロジェクト Sandcastle` と関係 `can_view` を持っている
{ user: "Jane", relation: "can_view", object: "project:Sandcastle"}

このようにオブジェクトへのアクセスをユーザーに直接付与する方法を Direct Access と呼びます。

ちなみに、モデルの読み方の個人的コツは

主語に来ることができるのは typedefine で定義されている 名詞 です。

このモデルは、最も基本的なモデルですが、ユーザーひとりひとりに対して個別にアクセスを与えるようなケースでしか使えません。

# ユーザー `Jane` はオブジェクト `プロジェクト Sandcastle` と関係 `can_view` を持っている
{ user: "Jane", relation: "can_view", object: "project:Sandcastle"}

# ユーザー `Bob` はオブジェクト `プロジェクト Watermemory` と関係 `can_view` を持っている
{ user: "Bob", relation: "can_view", object: "project:Watermemory"}

...

間接的に関係を定義する

ユーザーひとりひとりではなく組織の認可を考えるとき、多くの場合、ユーザーの役割に紐づくアクセス制御を実装します。

先ほどのモデルにロールを追加してみましょう。

model
  schema 1.1

type user

type project
  relations
    define owner: [user] // ロール追加
    define editor: [user]
    define viewer: [user]
    define can_edit: owner or editor // ロール対応するアクセス追加
    define can_view: viewer
    define can_delete: owner

関係タプルはこのようになります。

# ユーザー `Jane` はオブジェクト `プロジェクト Sandcastle` と関係 `owner` を持っている
{ user: "Jane", relation: "owner", object: "project:Sandcastle"}

# ユーザー `Bob` はオブジェクト `プロジェクト Sandcastle` と関係 `editor` を持っている
{ user: "Bob", relation: "editor", object: "project:Sandcastle"}

# ユーザー `Sara` はオブジェクト `プロジェクト Sandcastle` と関係 `viewer` を持っている
{ user: "Sara", relation: "viewer", object: "project:Sandcastle"}

...

ユーザーに直接アクセスを付与していませんが、ロールを介して間接的にユーザーにはアクセスが付与されています。

プレイグラウンドで動作確認を行うことができます。

モデルと関係タプルを設定した後、以下のようなクエリを流すことでアクセス制御を視覚的に確認することができます。

# Jane はプロジェクト Sandcastle に対して can_edit の関係を持っていますか?
is user:Jane related to project:Sandcastle as can_edit?

このように結果が表示されます。 can_editJane だけでなく Bob も実行できることが分かります。

では Viewer である Sara はどうでしょうか?

Saracan_edit の関係にないことが表示されました。

このようにアクセス制御を間接的に解決するプロセスを chain of resolution と呼んでいます。

これはロール・ベースド・アクセス・コントロール(Role-Based Access Control, RBAC)と呼ばれているものと似ている考え方です。

OpenFGAではこのようなモデルを定義することも可能なのですが、OpenFGAの本当の強みである「きめ細やかなアクセス制御」はここからです。

(注)ここでは分かりやすく従来のロール・ベースド・アクセス・コントロールと同じ考え方で説明していますが、OpenFGAでは、正確には「関係」を示しているに過ぎません。

ユーザーグループを定義する

OpenFGAは、任意のユーザーグループに対してアクセス制御することができます。先ほどのロールはシステム稼働前に開発者によってあらかじめ作成しますが、ユーザーグループはシステム稼働中にユーザーによって新しく作成されます。

例えば、GoogleドライブのグループやSlackのユーザーグループ、X(旧Twitter)のフォロワーもユーザーグループだと言うことができます。

では、そのモデルを見ていきましょう。

model
  schema 1.1

type user

type project
  relations
    define owner: [user, group#member] // 主語にユーザーグループを追加
    define editor: [user, group#member]
    define viewer: [user, group#member]
    define can_edit: owner or editor
    define can_view: viewer
    define can_delete: owner

type group // オブジェクトにユーザーグループを追加
    relations
        define member: [user, group#member]

関係タプルは以下のようになります。

# ユーザー `Jane` はオブジェクト `Ancientグループ` と関係 `member` を持っている
{ user: "Jane", relation: "member", object: "group:Ancient"}
# ユーザー `Bob` はオブジェクト `Futureグループ` と関係 `member` を持っている
{ user: "Bob", relation: "member", object: "group:Future"}

# ユーザー `Ancientグループのメンバー` はオブジェクト `Scarletプロジェクト` と関係 `owner` を持っている
{ user: "group:Ancient#member", relation: "owner", object: "project:Scarlet"}
# ユーザー `Futureグループのメンバー` はオブジェクト `Violetプロジェクト` と関係 `owner` を持っている
{ user: "group:Future#member", relation: "owner", object: "project:Violet"}

JaneAncient のメンバーです。 Ancient のメンバーは プロジェクト Scarlet に参加しています。

オブジェクトの階層関係を定義する

OpenFGAは、オブジェクト間の関係を定義できます。そうすることで、ユーザーと1つのオブジェクトの関係が別のオブジェクトとの関係に影響を与える可能性があることを示すことができます。

例えば、Googleドライブのドキュメントとフォルダの関係がそうです。そのモデルと関係タプルを見ていきます。

model
  schema 1.1

type user

type document
  relations
    define parent: [folder]
    define editor: [user]
    define viewer: [user]
    define can_edit: editor or editor from parent
    define can_view: viewer or editor or editor from parent

type folder
  relations
    define parent: [folder]
    define editor: [user] or editor from parent

Scarlet > Ancient > Kodai という階層関係にします。

Jane は、 Scarlet フォルダに対して editor の関係にあります。

# ユーザー `Jane` はオブジェクト `Scarletフォルダ` と関係 `editor` を持っている
{ user: "Jane", relation: "editor", object: "folder:Scarlet"}

# ユーザー `Scarletフォルダ` はオブジェクト `Ancientフォルダ` と関係 `parent` を持っている
{ user: "folder:Scarlet", relation: "parent", object: "folder:Ancient"}

# ユーザー `Ancientフォルダ` はオブジェクト `Kodaiドキュメント` と関係 `parent` を持っている
{ user: "folder:Ancient", relation: "parent", object: "document:Kodai"}

chainした結果、 JaneKodai に対して editor の関係にあることが確認できました。


ここまで、公式ドキュメントのモデリングガイドの

について説明が終わりました。ここまで来れば、冒頭の簡易版のGoogleドライブのモデルと関係タプルが読めるようになりグッと理解が進むと思います。

感想

OpenFGAがきめ細やかなアクセス制御ができると言われている理由が、ユーザーグループやオブジェクトの階層関係をモデルに落とし込める点にあると思います。

モデリングガイドはまだ残っていて

  • Blocklists
  • Public Access
  • Multiple Restrictions
  • Custom Roles
  • Conditions

は、本記事の後編として後日投稿します。

OpenFGAのモデリングについて、より深く理解したい方は、ぜひ公式ドキュメントをご覧になってください。

以上です。