電通総研 テックブログ

電通総研が運営する技術ブログ

Casbinによる権限管理の実装

こんにちは、クロスイノベーション本部 AI トランスフォーメーションセンター所属の山田です。

今回はアプリケーションの認可(Authorization)に関して、Casbin というライブラリを使った設計・実装で紹介したいと思います。

認可(Authorization)と認証(Authentication)について

認可の話をするうえで前提として認可(Authorization)と認証(Authentication)の違いについて再確認しておきます。

認可(Authorization)は認証(Authentication)と混同されがちですが、区別して考えるのが重要です。

アプリケーションの認証は「ユーザーが誰であるか」を確認する役割を果たします。
一方で、認可は「ユーザーが何ができるか」を判断します。

最近のアプリケーションにおいて認証は、IdP(Identity Provider)に委譲するケースが多く、アプリケーションは IdP から発行されたトークンを検証することで「ユーザーが誰であるか」の認証部分を実装できるようになりました。
一方で認可は依然としてアプリケーション固有のロジックです。
理由は単純で、アプリケーションの中で各ユーザーがどんな操作ができるかはサービスごとに異なるからです。
これらはアプリケーションごとに設計し、実装する必要があります。

認可ロジックの基本要素

実際にシステムで認可を扱う際に最も基礎となる要素を分解すると「ユーザー」、「リソース」、「操作」の 3 つになります。

要素 意味
ユーザー 操作を試みる主体
リソース 操作対象のオブジェクト
操作 リソースに対して行いたい操作

認可における「ユーザー」「リソース」「操作」の関係

具体例にこの 3 つの要素を当てはめて、認可というものが何かを考えてみましょう。

「アリス(ユーザー)が文書 1(リソース)を閲覧(操作)できるか?」

認可とは、この問いに対して 「Yes」か「No」 かを判断する仕組みです。
つまり、認可は「誰が(ユーザー)」「何に対して(リソース)」「何をするか(操作)」の三要素の組み合わせで構成されるルールセットといえます。

このように要素を分解して整理することで、アプリケーションにおいて認可の設計を具体的なドメイン設計の一部として考えることができるようになります。
アプリケーション固有のルールも、最終的にはこの三要素の組み合わせとして表現できます。

Casbin とは

Casbin はここまで紹介してきた認可の処理をアプリケーションに実装する際に利用できるライブラリです。
Casbin は非常に柔軟なライブラリで大体の認可ロジックを実装できます。
オリジナルの実装は Go 言語ですが、JavaPythonJavaScript など他のプログラミング言語の実装もあります。

casbin.org

Casbin の概念

Casbin 利用時は 「Model」、「Policy」、「Enforcer」 という 3 つの要素からなります。
これらの 3 要素の意味はそれぞれ以下です。

要素 意味
Model アクセス制御のモデルを定義するもの
Policy 権限データの実体
Enforcer Model と Policy を読み込み、要求に対し認可ロジックを実行するエンジン

Casbinにおける「Model」、「Policy」、「Enforcer」の関係

Model はアクセス制御の論理的な骨組みを定義します。
アプリケーションに組み込む場合、Model は静的であり、model.confのようなファイルベースで管理されます。

Policy は Model 側で決められた構造に従った実際の権限データの実態です。
こちらは Model とは対照的に動的であることがほとんどです。
Policy もpolicy.csvのようにファイルベースで管理できますが、データベースでの管理もできます。

Casbin でのアクセス制御モデル

Casbin では「Request」、「Policy」、「Matcher」、「Effect」 という 4 つの概念を使ってアクセス制御モデルを構成します。

要素 意味
Request Request は認可判断の入力で、操作を行う主体(sub)、 アクセス先のオブジェクト(obj)、実施する操作(act)が含まれます。
Policy Policy は権限データの構造を定義します。基本的にはp={sub, obj, act}ですが、ポリシーのマッチング結果の eft 列を明示的に追加し、p={sub, obj, act, eft}にもできます。
Matcher Matcher は Request と Policy のマッチングルールを定義します。
Effect Effect は最終的な認可の決定ルールを定義します。

実際のアクセス制御モデルの構成を見ていきましょう。
最も基本のアクセス制御モデルである ACLAccess Control List:アクセス制御リスト)は以下のようになります。

# Request definition
[request_definition]
r = sub, obj, act

# Policy definition
[policy_definition]
p = sub, obj, act

# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

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

ポリシーに以下の権限データが格納されているとします。

p, alice, data1, read
p, bob, data2, write

この場合、Casbin では以下のように判断できます。

  • alice は data1 を read できます。
  • bob は data2 を write できます。

ここまでが Casbin のアクセス制御モデルの基本になります。

Casbin を実際に使ってみる

Casbin の概念についてはある程度触れたので実際に Casbin を使ってみましょう。
今回は Casbin の Python 実装である PyCasbin を利用します。

github.com

pip install pycasbin

model.confpolicy.csvを用意し、それぞれをロードします。

import casbin

# Enforcerの初期化
e = casbin.Enforcer(model="./model.conf", adapter="./policy.csv")

enforcerメソッドを使って権限のチェックを実施します。

# 権限のチェック
# 例: "alice"が"data1"に対して"read"権限を持っているか確認
result_1 = e.enforce("alice", "data1", "read")
assert result_1 == True

# 例: "bob"が"data2"に対して"write"権限を持っているか確認
result_2 = e.enforce("bob", "data2", "write")
assert result_2 == True

# 例: "alice"が"data2"に対して"write"権限を持っているか確認
result_3 = e.enforce("alice", "data2", "write")
assert result_3 == False

このようにして、Casbin ではアクセス制御モデルのルールをもとに権限チェックを行えます。

権限データ(Policy)をデータベースから読み取る

次に、より実践的なパターンとして権限データをデータベースから読み取る例を紹介します。
今回は RDBPostgreSQL を使って権限データを管理します。

説明を省いていましたが、Casbin では Policy の読み取りは、Adapter というモジュールを介して行われます。

CasbinでのAdapterモジュールの位置付け

PyCasbin で権限データの管理に RDB を扱う場合には、SQLAlchemyベースでDBとやり取りをするcasbin_sqlalchemy_adapterという Adapter モジュールの利用がおすすめです。

github.com

今回は上記のcasbin_sqlalchemy_adapterPostgreSQL への接続に使うためにpsycopg2の2つのライブラリを依存関係に追加します。

pip install psycopg2
pip install casbin_sqlalchemy_adapter

PostgreSQL を使うために、Adapter で PostgreSQL への接続情報を与えます。

import casbin_sqlalchemy_adapter

DB_USER_NAME = "<データベースのユーザー名>"
DB_PASSWORD = "<データベースのパスワード>"
DB_HOST = "<ホスト名 or IPアドレス>"
DB_NAME = "<データベース名>"
adapter = casbin_sqlalchemy_adapter.Adapter(f"postgresql+psycopg2://{DB_USER_NAME}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}")
e = casbin.Enforcer("./model.conf", adapter=adapter)

casbin_sqlalchemy_adapterでは、上記のように設定すると PostgreSQL のデータベース上にcasbin_ruleテーブルが自動で作成されます。
作成されるcasbin_ruleテーブルは以下のような構造になります。

 Column |          Type          | Collation | Nullable |                 Default
--------+------------------------+-----------+----------+-----------------------------------------
 id     | integer                |           | not null | nextval('casbin_rule_id_seq'::regclass)
 ptype  | character varying(255) |           |          |
 v0     | character varying(255) |           |          |
 v1     | character varying(255) |           |          |
 v2     | character varying(255) |           |          |
 v3     | character varying(255) |           |          |
 v4     | character varying(255) |           |          |
 v5     | character varying(255) |           |          |
Indexes:
    "casbin_rule_pkey" PRIMARY KEY, btree (id)

Casbin ではポリシーデータを書き込むメソッドも用意されているため、こちらを利用して、動的に権限データをデータベースに永続化できます。

# 権限データの書き込み
added = e.add_policy("alice", "data1", "read")
# 新規行追加の場合はTrueが返る
assert added == True

# 権限のチェック
result = e.enforce("alice", "data1", "read")
assert result == True

より実践的な使い方(1):RBAC

ここまでで ACL ベースのアクセス制御を見てきましたが、実際のアプリケーションではロールベースのアクセス制御である RBAC が用いられることが多いので、RBAC のパターンも紹介します。
RBAC ではmodel.confに以下のようにrole_definitionを追加し、matchersでロールまで見るように設定します。

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

# ロールの継承関係の定義
[role_definition]
g = _, _

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

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

RBAC ではロールに対し、操作権限を付与する形になります。
今回はeditorreaderという 2 つのロールで考えてみましょう。
editordataに対しreadwriteが可能、readerdataに対し、readだけが可能とします。

一度、権限データはすべて削除した状態とします。

# ロール定義を追加
e.add_policies(
    [
        ["editor", "data", "write"],
        ["editor", "data", "read"],
        ["reader", "data", "read"],
    ]
)

この状態でaliceにはeditorロール、bobにはreaderロールを付与します。

# ユーザーにロールを割り当て
e.add_role_for_user("alice", "editor")
e.add_role_for_user("bob", "reader")

ここまででテーブルは以下の状態になります。

id ptype v0 v1 v2 v3 v4 v5
1 p editor data write NULL NULL NULL
2 p editor data read NULL NULL NULL
3 p reader data read NULL NULL NULL
4 g alice editor NULL NULL NULL NULL
5 g bob reader NULL NULL NULL NULL

この状態で権限チェックを行います。

# 権限のチェック
result_1 = e.enforce("alice", "data", "write")
assert result_1 == True

result_2 = e.enforce("alice", "data", "read")
assert result_2 == True

result_3 = e.enforce("bob", "data", "read")
assert result_3 == True

result_4 = e.enforce("bob", "data", "write")
assert result_4 == False

RBAC の重要なポイントはロールに対しリソースへの操作権限が割り当てられていることです。

RBACのイメージ図

アプリケーションの認可ロジックにおいて RBAC を採用することで、権限制御がユーザーに対して割り当てるロールの変更を行うだけで実現でき管理コストの軽減が見込めます。

ロールの継承について

ここまでで紹介したeditorのロールはredaerの上位ロールと考えることができます。
これを図で表現すると以下のようになります。

RBACにおけるロールの継承のイメージ図

Casbin では、このようなロールの継承も表現できます。

具体的に見ていきましょう。
editorロールでdataに関するread操作の権限情報を消し、editorロールがreaderロールを継承するように設定をします。

# Editorロールからread権限を削除
e.delete_permission_for_user("editor", "data", "read")
# editorロールをreaderロールの上位に設定
e.add_role_for_user("editor", "reader")

ここまででテーブルは以下の状態になります。

id ptype v0 v1 v2 v3 v4 v5
1 p editor data write NULL NULL NULL
3 p reader data read NULL NULL NULL
4 g alice editor NULL NULL NULL NULL
5 g bob reader NULL NULL NULL NULL
6 g editor reader NULL NULL NULL NULL

この状態でalicedatareadできるかを問い合わせると、Trueが返ることが確認できます。

# 権限のチェック
result = e.enforce("alice", "data", "read")
assert result == True

ユーザーとロール定義の重複への対処

Casbin では権限の設定はすべて文字列ベースで行われるためロール、ユーザーの区別がありません。
これはすなわちeditorreaderというユーザーがいる場合、ロール定義と重複してしまうため正しく権限チェックが行えなくなります。

そこで実際にアプリケーションで権限設定を行う場合は定義にプレフィックスをつけるなどで対応するのが良いでしょう。

例えばロール定義にはrole:、ユーザーの定義にはuser:のようなプレフィックスを付けます。
このようにすることで重複に対応できます。

# 権限情報の定義
e.add_policies(
    [
        ["role:editor", "data", "write"],
        ["role:reader", "data", "read"],
    ]
)

# ロール情報の定義
e.add_role_for_user("role:editor", "role:reader")
e.add_role_for_user("user:alice", "role:editor")
e.add_role_for_user("user:bob", "role:reader")

グループ管理への応用

ロールの継承はユーザーを集合したグループへのロール定義に応用できます。
ユーザーはグループに属し、グループに対してロールを付与するというシナリオを考えてみましょう。

今回はaliceはグループredに所属していて、グループredにロールeditorが与えられているとします。 これを図で表現すると以下のようになります。

グループに対してロールを付与する場合のイメージ図

先に紹介したユーザーとロール定義の重複を考慮して、グループの情報にはgroup:というプレフィックスを付けます。

# 権限情報の定義
e.add_policies(
    [
        ["role:editor", "data", "write"],
        ["role:reader", "data", "read"],
    ]
)

# ロール/グループ情報の定義
e.add_role_for_user("role:editor", "role:reader")
e.add_role_for_user("group:red", "role:editor")
e.add_role_for_user("user:alice", "group:red")

ここまででテーブルは以下の状態になります。

id ptype v0 v1 v2 v3 v4 v5
1 p role:editor data write NULL NULL NULL
2 p role:reader data read NULL NULL NULL
3 g role:editor role:reader NULL NULL NULL NULL
4 g group:red role:editor NULL NULL NULL NULL
5 g user:alice group:red NULL NULL NULL NULL

aliceはグループredに所属しているため、editor権限を持ち、dataの読み書き操作ができます。

# 権限のチェック
result_1 = e.enforce("user:alice", "data", "write")
assert result_1 == True
result_2 = e.enforce("user:alice", "data", "read")
assert result_2 == True

その他に、ユーザーが持つロール、所属しているグループの一覧はget_roles_for_userメソッドで取得可能です。

# ユーザーが所属しているグループ・付与されているロールの一覧の取得
rolls = e.get_roles_for_user("user:alice")
assert rolls == ["group:red", "role:editor"]

なお上記結果からも分かる通り、Casbin はグループ・ロールを区別しないため、所属しているグループの情報やロールの情報を抽出したい場合は、プレフィックスまで見る必要があります。

より実践的な使い方(2):権限データのフィルタリング

ここまでのやり方では、権限データを読み取るためにテーブルをすべて読み取る必要がありました。
これは少量のデータでは問題になりませんが、運用を見据えてデータ量が増えていくとパフォーマンス面で問題を引き起こす可能性があります。

そこで Casbin では権限データをフィルタリングして必要な権限データだけを読み取る機能をサポートしています。
一部のアダプタ実装では、フィルタされた権限データのロードをサポートしていません。

model.confを編集し、特定の関心事(ドメイン)での権限データであることを権限判定ロジックに加えます。
この時、データをフィルターしやすいようにptypepgのどちらの場合でも同じ列にドメインの情報が入るようにするのが良いです。

# リクエスト定義にドメインを追加
[request_definition]
r = sub, obj, act, domain

# 権限データの定義にドメインを追加
[policy_definition]
p = sub, obj, domain, act

# ロール定義にドメインを追加
[role_definition]
g = _, _, _

# 判定ロジックでドメインの一致を追加
[matchers]
m = g(r.sub, p.sub, r.domain) && r.domain == p.domain && r.obj == p.obj && r.act == p.act

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

この状態で、それぞれのドメインごとに権限データを登録します。

# 2つのドメインを定義
domain_1 = "alpha"
domain_2 = "beta"
# ドメインごとのロールと権限を追加
e.add_policies([
    ["role:editor", "data", domain_1, "write"],
    ["role:reader", "data", domain_1, "read"],
    ["role:editor", "data", domain_2, "write"],
    ["role:reader", "data", domain_2, "read"],
])

# aliceにはdomain_1のeditorロールを、bobにはdomain_2のeditorロールを割り当て
e.add_role_for_user_in_domain("user:alice", "role:editor", domain_1)
e.add_role_for_user_in_domain("user:bob", "role:editor", domain_2)

ここまででテーブルは以下の状態になります。

id ptype v0 v1 v2 v3 v4 v5
1 p role:editor data alpha write NULL NULL
2 p role:reader data alpha read NULL NULL
3 g user:alice role:editor alpha NULL NULL NULL
4 g user:bob role:editor beta NULL NULL NULL

ドメインalphaの権限の検証の際にドメインbetaの情報は不要であるため、フィルタしてロードすることが考えられます。
この時に使うのがload_filtered_policyメソッドです。
アダプタ側で用意されているFilterクラスを使って、条件を設定し、ロードする権限データを絞り込むことができます。

from casbin_sqlalchemy_adapter.adapter import Filter

domain_1 = "alpha"
domain_2 = "beta"
f = Filter()
f.v2 = [domain_1]

# domain_1のポリシーのみをロード
e.load_filtered_policy(filter=f)

# 権限チェック
result_1 = e.enforce("user:alice", "data", "write", domain_1)
assert result_1 == True

# domain_2のポリシーはロードされていないため、bobには権限がない状態になる
bob_roles = e.get_roles_for_user_in_domain("user:bob", domain_2)
assert bob_roles == []

まとめ

本記事では、アプリケーションの認可を組み込む際に利用できる Casbin というライブラリを紹介しました。
Casbin を利用することで、ロールの継承やグループへの権限の割当なども比較的にシンプルに実装できます。
アプリケーションの認可機能を実装する際には、候補として検討してみてはいかがでしょうか。


私たちは一緒に働いてくれる仲間を募集しています!

電通総研 キャリア採用サイト 電通総研 新卒採用サイト

執筆:@yamada.y
レビュー:@miyazawa.hibiki
Shodoで執筆されました