🛡️

AuthZEN Interopをcasbinとogenで実装する

2025/01/02に公開

はじめに

AuthZEN WGはOpenID Foundationにてアクセス制御のためのプロトコル標準化を目指すために2023年10月に発足されたワーキンググループです。2024年9月にAuthorization API v1.0がimplementer's draftとして発行されており、AuthZEN Interopにて相互運用性を確認するためのテストベッドが提供されています。今回は、AuthZEN Interopで例示されているToDoアプリケーションのアクセス制御を実現するためにcasbinとogenを使ってAuthorization APIを実装しました。
こちらでOPA実装した例が紹介されていますが、私はcasbinを好んで使うので実装の素振りをしたかったのと、ogenを使ったAPI実装を試したいと思いやってみました。

https://authzen-interop.net/docs/scenarios/todo-1.0-id/

https://github.com/openid/authzen/tree/main

Authorization API v1.0

モデルとインターフェースの概要

Authorization APIの仕様では、サブジェクト、リソース、アクションの4つのモデルが定義されています。サブジェクトとリソースはそれぞれ、認可判断の対象となるアクセス主体(例えばユーザー)とアクセス対象(例えばユーザー情報やToDo情報など)を表します。ドラフトではこれら2つのモデルをtypeidpropertiesで表現します。例えば、以下のようなサブジェクトの例がドラフトにて挙げられています。

{
  "type": "user",
  "id": "alice@acmecorp.com",
  "properties": {
    "department": "Sales",
    "ip_address": "172.217.22.14",
    "device_id": "8:65:ee:17:7e:0b"
  }
}

また、リソースの例として以下のようなものが挙げられています。

{
  "type": "book",
  "id": "123",
  "properties": {
    "library_record":{
      "title": "AuthZEN in Action",
      "isbn": "978-0593383322"
    }
  }
}

アクションはnamepropertiesで表現されます。例えば、以下のようなアクションの例がドラフトにて挙げられています。

{
  "name": "can_read"
}

また、共通的なアクション表現としてcan_accesscan_readcan_updatecan_updateといったアクション名が定義されています。

上記のモデル定義に加えて、アクセス判断のコンテクストを含むことができるようにリクエストパラメータとして定義されています。これらのモデルから、アクセス判断を依頼するリクエストボディは以下のように表すことができます。

{
  "subject": {
    "type": "user",
    "id": "alice@acmecorp.com"
  },
  "resource": {
    "type": "account",
    "id": "123"
  },
  "action": {
    "name": "can_read",
    "properties": {
      "method": "GET"
    }
  },
  "context": {
    "time": "1985-10-26T01:22-07:00"
  }
}

ogenによるAPIサーバーのコード生成

上記のモデル定義およびレスポンスデータ定義に基づいてOpenAPIスキーマを書き起こしました。
https://github.com/manaty226/authzen-interop-pdp/blob/main/openapi.yml

この定義から以下のコマンドの実行によりogenのサーバーインターフェースを生成できます。

ogen -package server -target server -clean ./openapi.yml 

ハンドラーインターフェースとしてEvaluateAccessメソッドができているので、これを実装することでAuthorization APIのサーバーができあがります。関数に渡される引数の型EvaluateAccessReqは上述したモデル定義がGoの構造体として定義されているので任意のアクセス制御ライブラリも実装しやすくなります。

type Handler interface {
	// EvaluateAccess implements evaluateAccess operation.
	//
	// Evaluate a user based on their profile.
	//
	// POST /access/v1/evaluation
	EvaluateAccess(ctx context.Context, req *EvaluateAccessReq) (*EvaluateAccessOK, error)
}

CasbinによるAuthZEN Interopの実装

それではAuthZEN InteropのサンプルユースケースであるToDoアプリケーションのアクセス制御をCasbinで実装してみます。このサンプルアプリケーションでは、以下の4つのロールが定義されています。

  • viewer: ToDoリストを閲覧することができるロール。また、各ユーザー情報も閲覧可能。
  • editor: viewerの権限に加えて、ToDoの作成と、自身がオーナーのToDoの編集権限を持つ。
  • admin: editorの権限に加えて、任意のToDoの削除権限を持つ。
  • evil_genius: editorの権限に加えて、任意のToDoの編集権限を持つ。

これらのロールは別途定義されている5人のユーザーに割り当てられています。このようなユースケースに対して、以下のようなcasbinモデルとポリシーリストを定義しました。

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act, rule

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub.ID,p.sub) && regexMatch(r.obj.ID, p.obj) && r.act == p.act && eval(p.rule)
p,viewer,user::*,can_read_user,true
p,viewer,todo::*,can_read_todo,(r.obj.OwnerID == r.sub.ID)
p,viewer,todo::*,can_read_todos,true
p,editor,todo::*,can_create_todo,true
p,editor,todo::*,can_update_todo,r.obj.OwnerID == r.sub.ID
p,editor,todo::*,can_delete_todo,r.obj.OwnerID == r.sub.ID
p,admin,todo::*,can_delete_todo,true
p,evil_genius,todo::*,can_update_todo,true

g,editor,viewer,,
g,admin,editor,,
g,evil_genius,editor,,

g,rick@the-citadel.com,admin,,
g,rick@the-citadel.com,evil_genius,,
g,beth@the-smiths.com,viewer,,
g,morty@the-citadel.com,editor,,
g,summer@the-smiths.com,editor,,
g,jerry@the-smiths.com,viewer,,

Casbinではevalによってポリシーごとに条件を指定することができるので、Authorization APIのpropertiescontextに含まれる情報を利用したABACもこれで表現することができます。また、階層ロールの表現も可能なため、「editorviewerの権限に加えて〜」のような状況も簡単に表現できます。
ただし、casbinで構造体を扱うのは内部実装としてリフレクションを利用しているため1.1倍~1.5倍程度のパフォーマンス低下が示唆されており、本番環境で利用する際には注意が必要です。パフォーマンスロスを最小限にするためには、subやobjをstring型に制約したり、contextを絞り込んでrequest_definitionを専用に定義しておくなど、柔軟性とのトレードオフを考慮してチューニングすることが必要です。

AuthZEN Interopテストの実行

AuthZEN InteropのGitHubリポジトリにはToDoアプリケーションのユースケースで定義されたアクセス判断を検証するテストケースも格納されているので、これを使って相互運用性を検証していきます。
まず、上述したogenから実装したAPIサーバーを以下のように起動します。

go run ./cmd/casbin/                                    
2025/01/02 19:24:34 INFO Server started at http://localhost:8080

次に、AuthZEN Interopのリポジトリをクローンしてテストを実行します。今回実行するのはdecisions-1.0-implementers-draft.jsonというファイルのAuthorization API v1.0 implementer's draftに対応したテストケースです。

git clone https://github.com/openid/authzen.git
cd interop/authzen-todo-backend
yarn install
yarn test http://localhost:8080

無事に全てテストがパスしました。

PASS REQ: {"subject":{"type":"user","id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"},"action":{"name":"can_read_user"},"resource":{"type":"user","id":"beth@the-smiths.com"}}
...(中略)...
PASS REQ: {"subject":{"type":"user","id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"},"action":{"name":"can_delete_todo"},"resource":{"type":"todo","id":"240d0db-8ff0-41ec-98b2-34a096273b95","properties":{"ownerID":"jerry@the-smiths.com"}}}

おわりに

AuthZEN InteropのToDoアプリケーションのアクセス制御をCasbinで実装し、ogenを使ってAuthorization APIのドラフト仕様に準拠したサーバーを実装しました。CasbinはABACをサポートしているため、Authorization APIのモデル定義に基づいて柔軟なアクセス制御を実現することができます。今回は簡単なユースケースだったのもあり労せず表現することができました。ogenを使ったAPIサーバーのコード生成も使用感がよく、標準仕様から型定義を作って実装する体験もスムーズで良いので今後も使っていきたいです。

Discussion