GolangでCasbin~アクセス制御ライブラリ~【Go】
What is Casbin
What Casbin does
- subject(主語)、object(目的語)、action(行動)をベースとして柔軟に「許可」「不許可」を定義することができる。
- RBAC(役割ベース)モデル(マルチテナント)など多様な権限設定が可能。
- adminユーザーを定義することができる。
- ルールの一致検証に使える演算子(関数)を提供。
- 例えば
keyMatch
は「/ foo / bar
がパターン/ foo *
にマッチする」と判定できる。
- 例えば
What Casbin does NOT do
- ログイン認証。
- あくまで文字列に対するルールを管理する(read == trueのような)。
- ユーザー自体の管理や役割(役割自体の定義)の管理。
How it works
.conf
形式で、構成ファイル(ポリシー記述の仕様書)を作成する。- 構成ファイルで定義した仕様に則り、
.csv
形式で役割、権限を定義する。 - サポートされている言語のCasbinライブラリを使って、構成ファイルとポリシーファイルを読み込み、trueかfalseを判定する。
- 各々のコードに、条件分岐によって、trueなら処理を許可する、falseならエラーを返すなどの処理を書く。
以下に、RBACモデルにおける例を書きます。
sample_configuration.conf
[request_definition] r = sub, dom, obj, act [policy_definition] p = sub, dom, obj, act [role_definition] g = _, _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
構成ファイルは、PERM(Policy/Effect/Request/Matchers)に則って記述します([request_definition]
、[policy_definition]
、[policy_effect]
、[matchers]
の4つのセクションが必要)。
RBAC(役割ベースのアクセス制御)モデルを利用する場合、セクション[role_definition]
も追加します。
[request_definition]
- Casbinのライブラリで、構成ファイルとポリシーファイルを読み込んでtrue or falseを判定する
Enforce()
メソッドの引数を定義します。 - つまり、「sub(subject)はobj(object)に対してact(action)できますか?」を問い合わせる書式です。
- Casbinのライブラリで、構成ファイルとポリシーファイルを読み込んでtrue or falseを判定する
[policy_definition]
- ポリシーファイルに記述する「誰が、何に対して、何ができるか(RBACの場合、テナント(domain)も記述)」というポリシーのフォーマットを定義します。
- 「誰が、〜」のように書きましたが、それは目的あって、全ての要素は単なる識別するための文字列で、それ自体に意味は持ちません。
- あくまで、ポリシーファイルの定義とリクエストを付き合わせることで「一致しているかどうか」を確認するときに意味を持ちます。
- なので、actは
read
ではなくてGet
と書いてもOKです。Enforce()
を使う際、[request_definition]のactに当たる部分にhttp.Methodを指定してrequestができるかどうかを判定させることができます。
- 執筆現在、Casbinは複数のポリシー定義パターンを許容していません。
- 例えば「dom(domain)を用いる場合」と「dom(domain)を用いない場合」の2つを同時に定義することは、今の所できません。
[policy_effect]
Effect()
などを使うリクエストの内容が、複数のポリシー定義にマッチする場合に、そのリクエストを許可するか拒否するかを定義します。some
は「あるかどうか」を意味します。where
で条件を判定します。p.eft
は「ポリシーのマッチ結果」を意味し、それが== allow
であれば、許可されている、== deny
であれば拒否されている、となります。- ただし、何でも書けるわけではありません。Casbinの作成者が「それほどの柔軟さは求められない」と判断したためです。次のパターンがハードコードされており、指定することが可能です(詳細)。
some(where (p.eft == allow))
!some(where (p.eft == deny))
some(where (p.eft == allow)) && !some(where (p.eft == deny))
priority(p.eft) || deny
[matchers]
[role_definition]
- type(上記例では
g
)と要求する個数(少なくとも2つ)を、カンマ区切りで_
を使って指定します。 - 「誰に、どのテナントの、何のポリシーを与えるか」や「誰に、どのテナントの全ての権限を与えるか」などを定義します。
- 上記のようなことは
[matchers]
によって判断します。
- type(上記例では
sample_policy.csv
p, admin, domain1, data1, read p, admin, domain1, data1, write p, admin, domain2, data2, read p, admin, domain2, data2, write g, alice, admin, domain1 g, bob, admin, domain2
pで表記されているポリシー定義と、gで表記されている役割定義は、それぞれ先述した下記の構成ファイルの定義に沿って書かれています。
[policy_definition] p = sub, dom, obj, act
1行目を解釈すると、
「adminは、domain1で、data1を、readできる」という意味になります。
(matchersが無ければ、あくまでただの識別子ですが、わかりやすさのために書いています)
[role_definition] g = _, _, _
役割定義の1行目を解釈すると、
「aliceは、adminであり、domain1に属する」となります。
(こちらも本来は、順序によって識別するただのマークです。)
サーバーサイドのコード
Go(with firebase authentication)を例に解説します。
※ 別ファイル、別パッケージとした方が良い部分がありますが、簡単のため、まとめています。
func main() { // 構成ファイルとポリシーファイルを読み込み、CasbinのEnforcerを生成 authEnforcer, err := casbin.NewEnforcerSafe("./auth_model.conf", "./policy.csv") if err != nil { log.Fatal(err) } //依存性 dependency(d)を注入 // NewMiddleware(svc Service, CasbinEnforcer: *casbin.Enforcer)の実行など // chi.Routerを使ってrouting r.With(d.FirebaseAuth.Handle).Route("/hoge", func(r chi.Router) { r.Get("/", d.HogeHandler.Get) }) } func (m *Middleware) Handle(next http.Handler) http.Handler { // 認証を行いたいパスへのアクセスを処理する何かしらのHandlerを想定 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { idToken := m.getAuthorizationHeader(r) // Headerから認証情報であるid token(firebase authentication)を取得するメソッドを別途定義 if idToken == "" { // エラー処理 return } okFromCasbin, err := m.Svc.Authentication(ctx, r, m.CasbinEnforcer, idToken) if err != nil { // unauthorizedエラーを返す return } if okFromCasbin { // Casbinからtrueが返されたので、routingを成功させる next.ServeHTTP(w, r) } else { // unauthorizedエラーを返す return } }) } func NewMiddleware(svc Service, CasbinEnforcer: *casbin.Enforcer) *Middleware { return &Middleware{ Svc: svc, CasbinEnforcer: *casbin.Enforcer } } type Claims struct { Role string } func (s *service) Authentication( ctx context.Context, r *http.Request, CasbinEnforcer *casbin.Enforcer, idToken string, ) (bool, error) { claims := Claims{} t, err := s.firebaseauthClient.VerifyIDTokenAndCheckRevoked(ctx, idToken) if err != nil { // エラーログ return userID, claims, err } if role, ok := t.Claims["Role"].(string); ok { claims.Role = role } // contextにClaimを詰める処理 // casbinにリクエスト return CasbinEnforcer.EnforceSafe(claims.Role, r.URL.Path, r.Method) }
Casbinには
- 設定内容を読み込む
- リクエストを送り、「許可」「不許可」を問い合わせる
メソッドがあるので、それを利用すればOKです。
「許可」が返されたら、serve HTTPをし、「不許可」ならUnauthorized(認証されていない)エラーを返します。
参考
Basic Role-Based HTTP Authorization in Go with Casbin - zupzup
Approach Question: Support license with roles · Issue #433 · casbin/casbin · GitHub
Add Permissions Requirement on route? · Issue #33 · casbin/casbin · GitHub