API での認可処理 (Authorization)

誰がどのように、システムが持つリソースを取得・変更して良いのかの権限チェックを認可処理といい、システム上では何らかの認可処理が必要となります。

認可処理について、チームによって異なる方法を取っていたり、共通の認識がなかったりすると、チェックの漏れや重複が発生し問題となります。本章では、マイクロサービスアーキテクチャを前提とした、単純な認可処理の方針を定めています。

TL;DR

  • 認可処理はどこで行うか? → 該当データのオーナーとなるマイクロサービスの API を境界として、API の実装に認可処理を含める。

  • 認可処理はいつ必要があるか? → デフォルトでは行うが、行わないことを API 単位で表明して認可処理をスキップすることもできる。

前提となる枠組み

APIエンドポイントの分類

全ての API のエンドポイントは、以下の二つのいずれかに分類できます。

exposable…呼び出し元のユーザーに基づいたデータの取得・変更の権限チェックが行われているもの(デフォルト)

unexposable…そのような制御が行われておらず、任意のデータの取得・変更が行えるもの ... internal

exposable な API は暗黙的に「呼び出し元のユーザー」という入力をインターフェイスに持ちます。例えば、プロフィール情報の取得 API であれば、つながりなどの公開範囲設定に応じて返すプロフィール情報が変わるように実装します。

unexposable な API は日時のバッチ処理からの呼び出し(システム内部での処理)や管理画面など、入力内容も含めて API 呼び出し全体が信頼できると仮定できる前提で用意するものです。

これらの分類は、直接的な認可処理に限らず、API の責務全体を定義します。例えば、ActiveRecord で生成される updated_at はシステムの内部情報なので、仕様として公開することを意図していない限り exposable な API で返すべきではありません。

デフォルトでは exposable なものとしてエンドポイントを分類・実装するようにします。これは「各マイクロサービスが自分自身の持つデータについて取得・変更の権限を確認するようにする(別のサービスに任せない)」ということでもあります。

一方で unexposable なものとして実装されたエンドポイントは、basic authentication(!= authorization)やネットワーク的に隠すなど、内部の者で信頼できることを示す何らかの証拠を求めるべきです。

システム全体の動作

我々の理想とするアーキテクチャにおいて、エンドユーザーからのAPI呼び出しはまず GraphQL gateway が引き受けます。言い換えれば、それ以外の API(システム API と呼ぶことにする)は全て隠蔽されていて、エンドユーザが直接叩くことができません。

この gateway は、cookie session や token などのクライアント情報を元に「呼び出し元のユーザー」を認証します。そして、システム API を認証済みの「呼び出し元のユーザー」を入力として叩きます。

システム API が他のシステム API を叩く場合は、バケツリレー方式でこの認証情報を伝搬し、同じインターフェイスで入力に含めます。

通常は、gateway が引き受ける API 呼び出しから間接的に呼び出される API は全て exposable なものとなる はずです。

規約

以上のような枠組みをサポートする、いくつかの規約を導入します。

規約1:API の種別の表明

API の種別を proto で次のように表明するようにします。skip_authorization = true であれば unexposable、skip_authorization = false であれば exposable、何も書かなければゼロ値で false なのでやはり exposable、となります。つまり、デフォルトは exposable とすることを意図しています。

rpc Echo(SimpleMessage) returns (SimpleMEssage) {
  option (wantedly.api.rpc).skip_authorization = true;
}

API を実装するコードレビューの観点では、skip_authorization = true という表明がない限り、呼び出し元のユーザーに応じて適切な取得・変更の制御を行なっていることをチェックすべきです。

API を呼び出すコードレビューの観点では、その呼び出し元の API 自体が信頼できる呼び出し元から呼び出されているのでなければ、skip_authorization = true の表明をしている API につなぎこまないようにチェックすべきです。

規約2:「呼び出し元ユーザー」の入力インターフェイス

「呼び出し元ユーザー」の入力は、x-current-user-id という名前の HTTP / gRPC ヘッダーに入れるようにします。"current user id" よりは "authenticated user id" の方が本来の意味と近いですが、既存のコードを鑑みてこのようにしています。

社内マイクロサービス共通ライブラリである servicex にも特別のインターフェイスを用意して、バケツリレーが必要な場合や、信頼できる呼び出し元からの実行を助けていくのが今後の方針です。実際に規約をどう実現するかに関しては、言語・F/Wごとにデザインすることになるので本章では触れません。

例:

YashimaApi.new(current_user_id: current_user.id).get(...)

規約3:認可に失敗した場合の表明

以下のステータスコードを返すようにします。

  • HTTP: 403(Forbidden)

  • gRPC: PERMISSION_DENIED

補足: 規約に含めないと判断した項目

以下は将来的に組織がスケールしたりプロダクトが成長した場合に導入される可能性があるものです。現時点ではコストに見合わないと判断しています。

  • 「スコープ」のような高級な抽象化 ... スコープのリストと意味自体をアプリケーションとシステムで意識する必要が出てくる

以下は案として出たが期待する問題をうまく解決してくれるかわからないのでひとまずやらないとなったものです。

  • 「会社」を認証対象にする(current-company-id のようなもの)... 複数の認証対象を取り扱う必要が出てくる

以下はやらない方が今後も良いだろうと判断しているものです。

  • current-user-id のような認証情報を暗黙的に伝播すること ... 例えば Sidekiq のようなバックグラウンドジョブに渡すときには、常に明示的に渡しているが困っていないため

規約の適用・実装に関して

方針と規約はアーキテクチャ上の決定として用意しますが、各言語・コーディングレベルでどのようにその規約を適用するかはある程度自由に考えることとします。

例えば、ある言語・フレームワークでは、unexposable な API を特定の名前空間にまとめる、ということが自然である場合はそのようにするのが良いです。実際に、wantedly/wantedly の gRPC services 以下は、そのような実装になっています。

話を聞きに行きたい

もっと知りたい

最終更新