どんな記事?
前回、Casbinで学ぶアクセス制御モデルRBACを書いたあとに、書き足りないなと感じていたので再度Casbinについて書いてみようかと。
引き続きCasbinとGoについて、業務で得た知見のメモ書きのようなものです、どこかの誰かの目に留まってお役に立てば幸いです。
要約
- Adapterについて
- 大量ポリシーの一括Loadに気をつけよう
- 階層型RBACポリシーの階層に気をつけよう
Adapterについて
Casbinはポリシーを管理するストレージをファイル、RDB、NoSQL等から選べます。
Adapterはストレージ種別に関わらずポリシーを読み書きできるようにしてくれるmiddlewareのような存在です、公式にもaka middleware for Casbin
とありますね。
Casbinにデフォルトで組み込まれているのはFileAdapterですが、私のプロジェクトでは一部Gormを使っていることからGormAdapterを使用しています。
その名の通りGormがサポートするRDBからポリシーをロードしたり、ポリシーをRDBに保存するためのAdapterです。
Gormのほかにもent.などのORMに対応したAdapterやS3に対応したAdapterもあるので、詳しくは公式も参照してもらえれば。
ポリシーストレージをファイル以外にすることの何が嬉しいかですが、ファイル管理だとアカウントデータ(=DB保存)とアカウントの権限ポリシーの保存場所が切り離されてしまい、管理が煩雑になったり、別途バックアップを考えなければいけなくなる、リアルタイムに読み書きする処理が煩わしい、というところかなと思います。
が、RDBに保存するということは当然ながらRDBの性能問題にも直面するということが起こります。
今回私が直面したのもこの問題で、下記のようなポリシーレコードの状態になってしまい、開発環境だけでも30000レコードを超えるテーブルが画面からのアクセスのたびにフルスキャンされてしまっている状態でした。
g, team1, data1_read,
g, team1, data1_write
g, team1, data2_read,
g, team1, data2_write
g, team1, data3_read,
g, team1, data3_write
g, team1, data4_read,
g, team1, data4_write
g, team1, data5_read,
g, team1, data5_write
g, team2, data1_read
g, team2, data1_write
...
...
※チームが増えるたびに対象リソース(data1...)のread/writeが追加されるため、1team追加する度に数百ほどのレコードが追加される状態
※簡略化したイメージで実際のデータとは異なります。
大量ポリシーのLoadに気をつけよう
仕様上、認可対象リソースが多くなったり、Roleを割り当てる対象(企業、ユーザー、チーム)等が多くなったりすると、Casbinのポリシーテーブルには大量のレコードが保存されていきます。
アカウント追加等でユーザーによって動的に追加されうるポリシーを、常に最新を参照するために毎回Loadするケースで、何も考えず全件取得すると、場合によっては一度に数万〜レコードをフルスキャンすることになり、非常に非効率的です。
前回紹介したような、APIアクセス時にmiddlewareでリソース認可を行っている場合は、1APIアクセスのたびにCasbinのポリシーテーブルをフルスキャンすることになるので、DB的にもAPIサーバーのプロセス的にもかなりリソースを使ってしまう状況となります。
私が担当していたプロダクトでもユーザーやリソースが増えたことにより、アカウント一覧、リソース一覧API等で権限のロード(≒DBアクセス)に時間がかかるようになってきて、画面からのリクエストに応えるのに2秒以上かかっていた状態となってしまっていました
どうすればよいか
結論から言うと、全件ではなくそのとき必要なPolicyのみLoadする、です。
当たり前っちゃ当たり前ですが、私が改修した既存コードは本当にあらゆるところでフルスキャンが走る作りになっており、
対象企業、対象ユーザー、対象グループ等での絞り込みがまったくされていませんでした。
Casbinでいう、LoadPolicyですね。
https://casbin.org/docs/policy-storage#adapter-api
これを、その時のアクセス者(ユーザー、グループ)の持つ権限のみをLoadするように変えてあげれば、フルスキャンではなくなり読み込み速度が大幅にアップします。
下記のLoadFiltererdPolicyです。
https://casbin.org/docs/management-api#loadfilteredpolicy
使い方はgithubのテストコードが参考になります。
読み込む対象の「ポリシータイプ(p:ポリシー/g:グループ)」と対象ポリシータイプ名を指定するだけです。
team1
という識別子で登録されているグループが持つポリシーのみフィルタリングするときは下記指定となります
※エラー処理は省略しています
enforcer.LoadFilteredPolicy(gormadapter.Filter{
Ptype: []string{"g"},
V0: []string{"team1"},
})
これでteam_1
以外のポリシーは読み込まれず、高速化&効率化できるようになりました🙌🏻
が、これで十分でしょうか、、、???
階層型RBACポリシーの階層に気をつけよう
実はこの指定だと前回の記事で紹介した、階層型RBACの場合、team_1
が直接持っている権限(=グループポリシー)しかLoadされないのです。
下記のポリシー定義にこのフィルターをかけるとdata1_read
とdata1_write
のみLoadされ、data1_read
やdata2_write
はLoadされません。
この状態でenforcer.Enforce("team1", "data1", "read")
してもfalseが返ってきてしまいます。
g, team1, data1_read
g, team1, data1_write
p, data1_read, data1, read
p, data2_read, data2, read
p, data1_write, data1, write
p, data2_write, data2, write
g:グループポリシー
が持つポリシーサブジェクトを持ってさらにp:ポリシー
をLoadしたいときは、
下記のようにLoadIncrementalFilteredPolicyを使います
// team1の持つグループポリシーの取得(data_read,data_write)
enforcer.LoadFilteredPolicy(gormadapter.Filter{
Ptype: []string{"g"},
V0: []string{"team1"},
})
// [[team1 data1_read][team1 data1_write]]
// team1の持つグループポリシーが持つグループポリシーの取得(data_read,data_write)
policyFilter := []string{}
for _, groupPolicy := enforcer.GetGroupingPolicy() {
policyFilter = append(additionalGroupPolicyFilterStrings, groupPolicy[1]) // [data1_read data1_write]
}
enforcer.LoadIncremeeFilteredPolicy(gormadapter.Filter{
Ptype: []string{"p"},
V0: policyFilter,
})
// [[data1_read, data1, read][data1_write, data1, write]]
これで晴れて、team1が持つポリシーを読み込むことができました。
あとはenforcer.Enforceで権限をチェックしてあげるだけですね。
isAllow, _ := enforcer.enforce("team1", "data1", "write")
// isAllow == true
最後に
私が扱っていた実際のデータでは3階層になっていたり、別のグループポリシーも読み込まなければいけなかったりと、上記に加えてさらにフィルタリング処理が多いコードになりましたが、改修前と後でAPIのレスポンスタイムを半分以下にまで抑えることに成功しました✨
あと、前回も書きましたが「役割の階層と権限設計」についてしっかり定義しておくと、ポリシーのFilteredLoadについても比較的容易に実装できるのかなと思います。
それではみなさま、幸せなCasbinライフを😌