概要
REST APIのAuthorizationをOPAに任せ、ミドルウェアなどで統一した処理にすることで認可処理の運用や柔軟性を向上させることができます。
今回はその実装方法を紹介します。
環境
- opa v0.32.0
- go v1.17
アーキテクチャ
アーキテクチャのイメージとしては以下です。
アクセスしたクライアントがその権限を持っているかOPAでチェックし、権限を持っていればアプリケーションサーバ側の処理④を実行します。
ポイント
REST APIのAuthorizationとして利用する際に考慮しておくと良いポイントを挙げます。
API仕様の変更に依存しない
認可システムを分離したのにAPIの些細な変更によって認可側も変更が必要になる(密結合)のはよろしくないです。なのでなるべく変更しない要素に対して依存させます。
APIの中で安定しているのは
- HTTP method
- URL path
です。
なぜならこれらが変更されるとクライアント側の互換性が失われるため、多くの場合一度決めたら変わることはありません。
なのでルールはそれらをベースにするのが良いです。
# GET /articles/:id allow { some id input.method = "GET" input.path = ["articles", id] }
変更が多いデータはOPA側に持たせない
RBACを用いた権限管理を行う場合、データ構造は
や
のような形になるでしょう。
基本的に権限(パーミッション)はAPIの機能自体が変わったりしない限りは変更されません。
その権限を束ねるロールも変更頻度は少ないでしょう。
一方でユーザは変更が多い要素であるため、Input側で管理し、送る形が良いでしょう。
ref: https://www.openpolicyagent.org/docs/latest/external-data/#option-2-overload-input
もしくはPush型にしてデータをリアルタイムに同期できる仕組みを用意するのが良いです。
ref: https://www.openpolicyagent.org/docs/latest/external-data/#option-4-push-data
同一ホストに置くもしくはサイドカーとして置く
ライブラリとしてではなく、別サーバとしてOPAを用意する場合は
- 可用性
- パフォーマンス
の観点から同一ホスト上に置くかサイドカーとして用意するのが良いです。
実装
それでは具体的な実装方法について説明します。
API
以下の5つのAPIがあるとします。
func main() { r := chi.NewRouter() r.Use(opaMiddleware) r.Route("/articles", func(r chi.Router) { r.Get("/", listArticles) r.Post("/", createArticle) r.Route("/{id}", func(r chi.Router) { r.Get("/", getArticle) r.Put("/", updateArticle) r.Delete("/", deleteArticle) }) }) http.ListenAndServe(":3000", r) }
各ハンドラにルーティングされる前にOPAで権限チェックをするミドルウェアを挟んでいます。
func opaMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() trimed := strings.Trim(r.URL.Path, "/") p := strings.Split(trimed, "/") // NOTE: コンテキストなどからユーザ情報の取得→ロールの設定 userRoles := []string{"article.editor"} input := data{ Input: input{ Method: r.Method, Path: p, Roles: userRoles, }, } body, _ := json.Marshal(input) resp, err := http.Post(opaEndpoint, "application/json", bytes.NewBuffer(body)) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } defer resp.Body.Close() res := result{} err = json.NewDecoder(resp.Body).Decode(&res) if !res.Result { w.WriteHeader(http.StatusForbidden) return } next.ServeHTTP(w, r.WithContext(ctx)) }) }
今回のコードではそのユーザがどのロールを持っているかはベタ書き(article.editor
)してます。
実際にはDBで保持し、ログイン後にJWTに持たせたりすると良いです。
OPAで権限チェックに失敗した場合は403
を返すようにしています。
OPA
次のOPA側のポリシーとデータの用意をします。
Data
以下のような権限と、それを束ねるロールを定義しています。
{ "role_permissions": { "article.admin": [ "article.create", "article.update", "article.list", "article.get", "article.delete" ], "article.editor": [ "article.create", "article.update", "article.list", "article.get" ], "article.viewer": [ "article.list", "article.get" ] } }
Policy
policy.rego
では以下のようなルールを用意しています。
先程定義した権限を持っているかをチェックしています。
default allow = false allow { user_is_admin } user_is_admin { input.roles[_] = "admin" } # GET /articles allow { input.method = "GET" input.path = ["articles"] has_permission(input.roles, "article.list") } # GET /articles/:id allow { some id input.method = "GET" input.path = ["articles", id] has_permission(input.roles, "article.get") } # POST /articles allow { input.method = "POST" input.path = ["articles"] has_permission(input.roles, "article.create") } # PUT /articles/:id allow { some id input.method = "PUT" input.path = ["articles", id] has_permission(input.roles, "article.update") } # DELETE /articles/:id allow { some id input.method = "DELETE" input.path = ["articles", id] has_permission(input.roles, "article.delete") } has_permission(roles, p) { r := roles[_] data.role_permissions[r][_] == p }
APIが増えるたびにルールが増えるのでそこは微妙ですが、URL pathをベースにチェックする形だと変数が入ってしまうのでこうしています。
gRPCのような固定のメソッド名でチェックできる場合は↓のようにルールを1つにまとめ、権限がルールに含まれないように(=Data側で)管理するのが良いでしょう。
allow = true { op = allowed_operations[_] input.method = op.method } allowed_operations = [ {"method": "myservice.ListArticles"}, {"method": "myservice.GetArticle"}, {"method": "myservice.CreateArticle"}, {"method": "myservice.UpdateArticle"}, {"method": "myservice.DeleteArticle"} ]
サンプルコード
今回のコードはこちらです。
動作確認
article.editor
なので取得・作成・更新ができます。
$ curl -i -XGET localhost:3000/articles HTTP/1.1 200 OK $ curl -i -XGET localhost:3000/articles/hoge HTTP/1.1 200 OK $ curl -i -XPOST localhost:3000/articles HTTP/1.1 200 OK $ curl -i -XPUT localhost:3000/articles/hoge HTTP/1.1 200 OK
一方で削除はできません。
$ curl -i -XDELETE localhost:3000/articles/hoge HTTP/1.1 403 Forbidden
期待通りの挙動ですね。
まとめ
REST APIのAuthorizationをOPAに委譲した場合の具体的な実装方法について説明しました。