Casbinで学ぶアクセス制御モデルRBAC

このエントリーは [3-shake Advent Calendar 2022](https://qiita.com/advent-calendar/2022/3-shake) 15日目の記事です。

## 自己紹介
* サーバーサイドメイン（フロント、インフラチョットだけ）で18年くらいエンジニアやってます
* 2022年10月に面白そうということで株式会社スリーシェイクにJoin
* インフラ構築運用もバリバリできる**スーパーエンジニア**になりたい
* 趣味はスノボ・ビール・うさぎ
* Qiita初投稿、Advent Calendar初参加なので乱文ご容赦ください

## どんな記事？
プロダクトで利用しているCasbinというライブラリを使う中で、
アクセス制御モデルのRBACについて学んだのでそのご紹介です。
こんなことができるんだーという感じで気軽に読んでいただければ！

## Casbinとは
ACL、RBAC、ABACなどの様々なアクセス制御モデルでアクセス制御（≒リソース認可）を行えるライブラリです。
バックエンドAPIではリソースに対してのアクセスを制御するために使われているのが主な用途かと思います。
※詳しくは[公式ページ](https://casbin.org/)をご参照ください。

## RBACとは
RBACとは、Role-Based　Access Controlの略で、ロールに応じたアクセス制御を行うモデルです。
「ユーザー」に対して、「管理者」・「一般」のような役割（ロール）を付与して、その役割に「何ができるか」の権限を与えていくモデルとなります。

分かりやすい例でいうと、エンジニアが業務で使っているであろうSlackも下記のようなロールがあり、それぞれに可能な操作が違うはずです。
* プライマリオーナー：ワークスペースに対する全ての権限を持つ
* 管理者：メンバー・チャンネルの管理権限を持つ
* 一般：チャンネル作成権限を持つ（削除は管理者以上）
* ゲスト：チャンネルでのメッセージ送信権限のみ

[参考:Slackメンバー種別](https://slack.com/intl/ja-jp/help/articles/201314026-Slack-%E3%81%A7%E3%81%AE%E5%90%84%E3%83%A1%E3%83%B3%E3%83%90%E3%83%BC%E7%A8%AE%E5%88%A5%E3%81%AE%E6%A8%A9%E9%99%90)

### 階層型RBAC
ロールは階層構造になっていることもあります。

会社の役割を考えたときに、
社長は会社の全てに対して権限を持ち、
部長は自身の部についての権限を持ち、
リーダーはチーム等で束ねているメンバーに対しての権限を持っています。
よく見る下記のような階層（組織）図ですね。
![rbac.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231246/af983d90-e086-ee58-0303-0e686210e643.png)

このとき、社長は部長の配下の部、部の配下のチーム、チームメンバーに対して権限を持っていないのでしょうか？
いえ、持っているはずです。
階層構造の上の役割は、下の役割の権限も兼ねるというのが、階層型RBACの考え方となります。

## Casbinによる階層型RBACの権限チェック
**ここからが本題**です。

### 役割・権限定義方法
RBACモデルをCasbinで設定する場合、
権限を表現する[ポリシー定義](https://casbin.org/docs/syntax-for-models#policy-definition)に加えて、役割を表現する[ロール定義](https://casbin.org/docs/rbac#role-definition)を追加してあげる必要があります。
※RBAC以外の各モデルとそのモデル定義の例は[公式](https://casbin.org/ja/docs/supported-models)を参照ください。

階層型でないフラットなポリシー定義が下記です。
```
p, data2_admin, data2, write
p, data2_admin, data2, read
g, alice, data2_admin
```
なんとなく伝わりそうですが、意味は下記です。
- ユーザー「alice」は役割「data2_admin」を持つ
- 役割「data2_admin」は「data2」の「write/read」権限を持つ

つまり、**「alice」は「data2」の「write/read」権限を持つ**ということですね。

ここに先程の階層型ロールの考え方を適用すると下記のようになります。

```
p, data2_admin, data2, write
p, data2_admin, data2, read
p, data1_admin, data1, write
p, data1_admin, data1, read
g, data_admin, data1_admin
g, data_admin, data2_admin
g, bob, data2_admin
g, alice, data_admin
```
これは、
「alice」は「data1/data2」の「write/read」権限を持ち、
「bob」は「data2」の「write/read」権限を持っているということになり、
**「alice」の持つ「data_admin」ロールは「bob」の持つ「data2_admin」ロールの上位階層に当たる**ことがわかります。

この階層をもう少し重ねてみるとどうなるでしょう。
```
p, data2_admin, data2, write
p, data2_admin, data2, read
p, data1_admin, data1, write
p, data1_admin, data1, read
p, team2_read, team2, read
p, team2_admin, team2, write
p, team2_admin, team2, read
p, team1_read, team1, read
p, team1_admin, team1, write
p, team1_admin, team1, read
p, payment_credit_user, a_credit, use-credit
p, payment_credit_user, b_credit, use-credit
p, payment_bank_user, a_bank, use-bank
p, payment_bank_user, b_bank, use-bank
g, payment_admin, payment_credit_user
g, payment_admin, payment_bank_user
g, data_admin, data1_admin
g, data_admin, data2_admin
g, team1_admin, team2_read
g, team2_admin, team1_read
g, team_admin, team1_admin
g, team_admin, team2_admin
g, admin, payment_admin
g, admin, data_admin
g, admin, team_admin
g, charlie, payment_creadit_user
g, carol, team1_admin
g, bob, payment_admin
g, alice, admin
```
自分で書いて後悔してきましたが、、、下記の意味になります。
- 「alice」は「admin」なので「data1/data2/team1/team2」の「write/read」権限を持ち、かつ「a_credit/b_credit」/a_bank/b_bank」の「use」権限を持つ
- 「bob」は「payment_admin」なので「a_credit/b_credit/a_bank/b_bank」の「use」権限を持つ
- 「carol」は「team1_admin」なので「team1」の「write/read」権限と「team2」の「read」権限を持つ
- 「charlie」は「payment_creadit_user」なので「a_credit/b_credit」の「use」権限を持つ

ロールと権限の階層としてはこのようなイメージです。
![rbac_hierarchy_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231246/9cf84e18-daf0-bc0c-e936-a19d35a7631f.png)

### 権限チェック方法
この権限定義を使って実際にCasbinを使って権限を問い合わせるときは下記のようになります。
※コード中の`policyFile`は上記の権限定義が書かれたCSVファイルのパスとなります。
※error処理は省略しています。
```Go
const rbacModel = `
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

# userid, role
[role_definition]
g = _, _

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

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && r.act == p.act
`
_model, _ := model.NewModelFromString(rbacModel)

enforcer, _ := casbin.NewEnforcer(_model, policyFile)
allowAlice, _ := enforcer.enforce("alice", "team1", "write")
allowBob, _ := enforcer.enforce("bob", "a_bank", "use_bank")
allowCarol, _ := enforcer.enforce("carol", "team2", "write")
allowCharlie, _ := enforcer.enforce("charlie", "b_bank", "use_bank")
// allowAlice: true
// allowAlice: true
// allowCarol: false
// allowCharlie: false
```
この`enforcer`をAPIのエンドポイント毎に組み込むミドルウェアに設定して各リソースへのアクセス制御に使ったり、各ユーザーの権限一覧を返すときにリソース/アクションに対してのallowをstructやmapに詰め込んで返してあげたりという使い方もできます。

どうでしょうか？
使い方としてはシンプルで良いライブラリだとは思っています。
…が、業務で既にある権限を改修した際に大変だったのは、「RBACの考え方とCasbinでのロール／ポリシーの表現方法に慣れる」ことと、権限定義、というか**階層ロール設計の複雑さ**がなかなか読み取れなかったのはなかなか辛かったポイントです。
おかげで、こんな記事が書けるぐらいに理解が深まったのはとても良かったですが😅

## 終わりに
実は前職でもCasbinは使っていたのですが、シンプルなACLモデルで使っていたのでここまで柔軟に役割と権限を管理できるものとは知りませんでした。
一方、RBACを使うとなると柔軟な分、「役割の階層と権限設計」をしっかり考えないと、複雑で煩雑な権限管理になってしまい、便利なライブラリなのに使ってる意味ないかも、と思ってしまうハメになりかねないのが難しいポイントかなとも思います。
今回RBACについてはそれなりに理解を深められたと思うので、もしまた今後の業務で関わることがあれば、少しでも使いやすい権限管理を構築していきたいです。

このような拙い記事を最後までお読みいただきありがとうございました！