はじめに
認可専用のライブラリであるCasbinを使って、ABACを実装してみる。Casbinでは、単純にルールを羅列するのではなく、eval()という組み込み関数を使用することで、任意の認可ロジックを簡潔に組み込むことができる。なお、CasbinそのものやRBAC/ABAC等の認可の概念に関する説明は本記事ではしない。
eval()では、公式ドキュメントの例のように直接ロジックを渡すこともできるし、AddFunction
というAPIを使ってGoで記述した任意の関数を登録し、それをポリシー内で呼び出すこともできる。今回は後者の方法を使い、DBへのアクセスが必要な権限管理のロジック、という想定で実装してみる。
実装するシナリオ
今回は、病院で使用するシステムを想定し、「自分が担当している患者のデータにのみアクセスできる」という要件を実装する。
実装
モデルの定義
model.conf
ファイルにモデルを定義する。
[request_definition]
r = user, obj, method
[policy_definition]
p = path, method, rule
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = keyMatch5(r.obj.Path, p.path) && (r.method == p.method || p.method == "*") && eval(p.rule)
ポリシー
policy.csv
にポリシーを記述する。
p, /patient/{patient_id}/record, *, "is_assigned_patient(r.user.ID, r.obj.ID)"
APIと認可用ミドルウェア
package main
import (
"fmt"
"net/http"
"strconv"
"github.com/casbin/casbin/v2"
"github.com/labstack/echo/v4"
)
var enforcer *casbin.Enforcer
func main() {
e := echo.New()
var err error
enforcer, err = casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
panic(err)
}
enforcer.EnableLog(true)
// 任意の関数をCasbinに登録する
enforcer.AddFunction("is_assigned_patient", func(args ...interface{}) (interface{}, error) {
// NOTE: Casbinは文字列型のみ対応している https://github.com/casbin/casbin/issues/113
doctorIDStr, ok := args[0].(string)
if !ok {
return false, fmt.Errorf("failed to get doctorID from casbin policy")
}
patientIDStr, ok := args[1].(string)
if !ok {
return false, fmt.Errorf("failed to get patientID from casbin policy")
}
doctorID, err := strconv.ParseUint(doctorIDStr, 10, 64)
if err != nil {
return false, fmt.Errorf("failed to parse doctorID: %w", err)
}
patientID, err := strconv.ParseUint(patientIDStr, 10, 64)
if err != nil {
return false, fmt.Errorf("failed to parse patientID: %w", err)
}
// DBから、リクエストを送った医者が担当している患者かどうかを取得する(今回はモック)
return isPatientAssignedToDoctor(int(doctorID), int(patientID)), nil
})
e.Use(authorize)
e.GET("/patient/:patient_id/record", func(c echo.Context) error {
return c.String(http.StatusOK, "Access Allowed\n\n")
})
e.Logger.Fatal(e.Start(":1323"))
}
type User struct {
ID int
}
type Object struct {
ID int
Path string
}
// 認可をミドルウェアで実施する
func authorize(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
path := c.Request().URL.Path
method := c.Request().Method
userIDStr := c.QueryParam("user_id")
userID, err := strconv.Atoi(userIDStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
objectIDStr := c.Param("patient_id")
objectID, err := strconv.Atoi(objectIDStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
obj := Object{ID: objectID, Path: path}
ok, err := enforcer.Enforce(User{ID: userID}, obj, method)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
if ok {
return next(c)
}
return c.String(http.StatusForbidden, "Access denied\n\n")
}
}
func isPatientAssignedToDoctor(doctorID int, patientID int) bool {
if doctorID == 1 && patientID == 1 {
return true
}
return false
}
注意点として、CasbinのポリシーはString型にしか対応していない。文字列型以外を渡すと正しく判定できないことがあるので注意が必要。
動作確認
// 自分が担当している患者の情報は取得できる
$ curl 'http://localhost:1323/patient/1/record?user_id=1'
Access Allowed
// 自分が担当していない患者の情報は取得できない
$ curl 'http://localhost:1323/patient/2/record?user_id=1'
Access denied