技術向上

プログラミングの学び、気になるテクノロジーやビジネストレンドを発信

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)できますか?」を問い合わせる書式です。


  • [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]
    • [request_definition][policy_definition]に記載されるのは、「書き方」であって、順番と識別のための情報(名前付きの箱)です。
    • [matchers]で両者を紐づけ、条件判定に使います。
    • +-*/のような算術演算子&&||!の論理演算子を使うことができます。
    • タイトルは複数形でありながら、複数のmatcherを定義することはできません。


  • [role_definition]
    • type(上記例ではg)と要求する個数(少なくとも2つ)を、カンマ区切りで_を使って指定します。
    • 「誰に、どのテナントの、何のポリシーを与えるか」や「誰に、どのテナントの全ての権限を与えるか」などを定義します。
    • 上記のようなことは[matchers]によって判断します。
      • 例えば右記について。m = g(r.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
      • 上記はg(r.sub, r.dom)の部分で、ポリシーファイルのgで指定されたものについて(例:g, bob, domain2)、1番目の要素(bob)がリクエストのsubと、2番目の要素(domain2)がリクエストのdomと文字列として等しいかをチェックします。

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(認証されていない)エラーを返します。


参考

Overview · Casbin

Casbin · An authorization library that supports access control models like ACL, RBAC, ABAC for Golang, Java, C/C++, Node.js, Javascript, PHP, Python, .NET (C#), Delphi, Rust, Ruby, Lua, Dart/Flutter and Elixir

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

How to implement RBAC custom function which has to read information from database? · Issue #326 · casbin/casbin · GitHub