認可のアーキテクチャに関する考察(Authorization Academy IIを読んで)
みなさま、認可の設計に苦しんでいるでしょうか?私は苦しんでいます。苦しまなかった瞬間などありません。昔「アプリケーションにおける権限設計の課題」を執筆しましたが、あれから3年以上が経ちます。
当時は認可の設計に関する情報がうまくまとまっている記事などほとんど無く、調べに調べて得たナレッジを書き記したのが上記の記事です。3年以上経ちますが、苦悩が今も特に変わっていないことが驚きです。
ただし、世の中的には認可のライブラリであったりサービスというのは少しずつ増えてきている印象があります(Auth0の OpenFGA であったりOsoの Oso Cloud 、Asertoの Topaz )。
認可の設計に関する記事も少しずつ増えている印象があり、その中でも本記事で紹介したいのがAuthorization Academyです。
これは認可サービスである Oso Cloud やOSSのライブラリ oso を提供しているOsoが出している認可に関するナレッジがシェアされている 神記事 です。この記事が昔存在していたら、どれだけ自分が救われただろうか、と思えるくらいにすばらしくまとまっている記事です。
英語が読める人は本記事を読むのをやめて、Authorization Academyを一読することを強くおすすめします。本記事ではAuthorization Academyの中のII章にあたる「What is Authorization?」に関する概要と、自分の経験をもとにしたナレッジも混ぜ込んでシェアできたらと思います。
いきなりまとめ
- 認可の対象が「誰」なのかは、認証にIdentity Provider(IdP)を使う
- 認可はアプリケーションに閉じた方がシンプル(専門の認可サービスを作るより)
- 認可ロジックはアプリケーションロジックとは分離する
- 分離できていないとバグりやすくなるし、スケールしにくい
- 認可に必要なデータは、そのサービス内(アプリのデータと同じDB)で持つ方がシンプル(巨大なサービスになるまでは)
- 複数サービスを作っている場合は、各サービスに認可のエンドポイントを持たせて認可を委譲できると良い
- 集約型の認可サービス(Authorization as a Service)にすると、認可に必要なデータの扱いが極めて複雑になっていく
概要
認可って?(What Authorization Looks Like in an App)
架空のサービスGitClub
Authorization Academyでは架空のサービスGitClubを例に考えています。GitClubはGitHubを想像してもらえたらOKです。
- GitClubのWebサイトにアクセスするときは
www.gitclub.dev
でアクセス- GitHubのWebページとかをイメージしてもらえたらOK
- サードパーティのアプリからAPIリクエストは
api.gitclub.dev
でアクセス- GitHub CLIだったりGitHubでPersonal Tokenを使うときにやりとりをするエンドポイントをイメージしてもらえたらOK
- Git CloneとかするときのGit自体とのやりとりは
git@git.gitclub.dev
のようにアクセス- GitHubのリポジトリを見つけて
git clone
するときのエンドポイントをイメージしてもらえたらOK
- GitHubのリポジトリを見つけて
GitClubのデータモデル
GitClubのデータモデルはシンプルで、Organization、Git Repository、Userの3つが登場します。
関係は以下の通り:
- Organizationは複数のGit Repositoryを持つ
- Organizationには複数のUserが所属する
- Userは複数のOrganizationに所属できる
この中で適用したい認可は:
「Userは、自分の所属しているOrganizationが持っているRepositoryにしかアクセスできない」
です。
認可はどこでどのように適用できるのか?(Where To Put Our Authorization Logic)
認可は一箇所で適用するものではなく、様々な場所で様々な角度から適用するものです。そして、様々な場所で繰り返し問い続けるのが認可三大要素(Three main aspects of authorization)です。
認可三大要素は以下のとおり:
-
「誰が」リクエストしているか
-
「何を」しようとしているのか
-
「何に」しようとしているのか
GitClubの例で、「ユーザーがRepositoryを参照する」リクエストがWebアプリに来た時に、どこで、どのように認可が適用できるのか認可三大要素の観点から考えます。
リクエストの入り口部分
これはロードバランサなど、リクエストのかなり初期フェーズの場所に該当します。
- 認可三大要素の確認
- 誰が
- この段階ではまだわからず、わかったとしてもIPアドレスくらい
- 何を
- TLS接続を行おうとしている
- 何に
- ホスト:
gitclub.dev
のポート443に対して
- ホスト:
- 誰が
- どのような認可ができる?
- この時点ではアプリケーションレベルの認可はできないが、ネットワークレベルの認可は可能(例えばIP制限など)
補足
アプリエンジニアにとってはあまり意識されないレイヤーかもしれませんが、IP制限やAWSのセキュリティグループなども認可にあたります
Proxy部分
ProxyはAWSでいうところのAPI Gatewayや、(筆者はあまり詳しくないですが)Nginxなどのレイヤにあたります。
- 認可三大要素の確認
- 誰が
- ここまで来るとリクエストに含まれるトークンを確認し、中身が抽出できる(e.g.
alice@acme.org
など)
- ここまで来るとリクエストに含まれるトークンを確認し、中身が抽出できる(e.g.
- 何を
- HTTPリクエストの種類が確認できるので、
GET
しようとしているかどうかなどが特定できる
- HTTPリクエストの種類が確認できるので、
- 何に
- アクセス先のURLのパス(e.g.
www.gitclub.dev/acme/anvil
)
- アクセス先のURLのパス(e.g.
- 誰が
- どのような認可ができる?
- ルーティングレベルの認可ならできる
- e.g.
alice@acme.org
は/acme/anvil
にGET
できる?
- e.g.
- ルーティングレベルの認可ならできる
補足
AWSを使ったことのある人はわかるかもしれませんが、API GatewayにはAuthorizerという仕組みがあり、そこでJWTの中身に応じて(例えばrole
や
WebアプリのRouter部分
これはWebアプリのいわゆるルーティング部分にあたります。例えばNode.jsのExpressで言うExpress Routerとか、渡ってきたリクエストを適切なハンドラーに委譲するレイヤーです。
- 認可三大要素の確認
- 誰が
- この時点ではRouterのミドルウェアがデータベースからUserに関連する情報を取得してUserオブジェクトみたいなものを作ることができる
- 何を
- この時点ではまだHTTPリクエストの種類が確認できる程度(e.g.
GET
とか)
- この時点ではまだHTTPリクエストの種類が確認できる程度(e.g.
- 何に
- 同じく、リクエストされたパスがわかる程度
- 誰が
- どのような認可ができる?
- やろうと思えばいくらでもできる(DBへのアクセスもできるので)
- ただし、Controller以降の処理でも同じようなことをする必要が出てくることが多いので、あまりこの時点では追加の情報は取得せずに認可を行う
- 例えば、Userオブジェクトに
isAdmin: true
があるなら、/admin
関連のパスへのアクセスを許可するかどうか認可できる
補足
この記事では詳しく触れませんが、Roleによって認可を適用する(Role-Based Access Control)などは、このレイヤーで「admin
Roleを持っているから許可する」のようなことをよくやります
WebアプリのController部分
ここがアプリケーションのコアな部分にあたります。
- 前提
- 前処理でRouterがControllerの
view_repository
メソッドにマップしたと仮定
- 前処理でRouterがControllerの
- 認可三大要素の確認
- 誰が
- Routerの段階でUserオブジェクトが既にあるので「誰」なのかはわかっている
- 何を
- Repositoryの情報を参照
- 何に
- 対象となるRepository(e.g.
acme/anvil
)
- 対象となるRepository(e.g.
- 誰が
- どのような認可ができる?
- ここまで来るとすべての情報がそろっているので認可可能
- 例: 「
anvil
Repositoryはacme
Organizationが持っていて、alice@acme.org
はacme
に所属している」ので「許可」されることになる
- 例: 「
- ここまで来るとすべての情報がそろっているので認可可能
補足
Controllerと言っているものの、レイヤーとしてはRouterが呼び出す先のアプリケーションのコア部分のレイヤーのことです。主にビジネスロジックが関わる部分になります
データアクセスの部分
アプリケーションにおいてデータ取得をするレイヤーにおいても認可をかけることができます。
- 認可三大要素の確認
- 誰が
- Routerの段階でUserオブジェクトが既にあるので「誰」なのかはわかっている
- 何を
- (RDBであれば)SQLのSELECTを実行
- 何に
- Repositoryテーブルから、対象となるRepository(e.g.
acme/anvil
)を絞り込んだもの
- Repositoryテーブルから、対象となるRepository(e.g.
- 誰が
- どのような認可ができる?
- SQLのクエリに認可を適用できる
- もともとのクエリが以下のように
acme/anvil
リポジトリの情報を取得していたとする
SELECT * FROM repositories r WHERE r.name = "acme/anvil";
- ここに
organization_member
テーブルがあったとして、JOINすることで「ユーザーは所属組織内のリポジトリのみ参照できる」認可を適用できる(具体的には以下)
SELECT * FROM repositories r JOIN organization_member om ON r.organization_id = om.organization_id WHERE r.name = "acme/anvil" AND om.member_id = $1; -- 所属組織内のRepositoryに絞る
- トリッキーなのは、上で追加したJOINやWHERE句が、SQLにハードコードしてしまっている点
- 認可ロジックとビジネスロジックの分離ができていません
- もともとのクエリが以下のように
- SQLのクエリに認可を適用できる
上記で認可の情報を「ハードコードしてしまっている」点については「アプリケーションにおける権限設計の課題」の「権限実装のアプローチ」でも紹介しています。「どこがハードコートなの?」と思う人はぜひ読んでみてください。
またOsoにはData Filterという仕組みがあり、これがなかなかおもしろいので興味がある人はこちらも深掘りしてみると良いです。
補足
簡単に書いていますが、このレイヤーでキレイに認可を適用するのはかなり難しいです。例えば上記でSQLに認可を適用していますが、「どのようなSQLなのか?」、「どんなテーブルをどんな名前でJOINしているのか?」、「カラム名はSQL内で変わっていないか?」など、不確定要素が多いからです。Prismaなど、型補完の効くORMなどと併用することである程度現実的に適用できますが、ORMの採用はそれはそれでデメリットもあったりするので、筆者はこのあたりのグッドプラクティスはまだ模索中です
以上で、「認可はどこでどのように適用できるのか」というのを各レイヤーの観点で理解できます。
認可のアプリへの組み込み方(Adding Authorization to an Application)
やってしまいがちな組み込み方
- 認可を始めから意識して作れることは少ない
- 認可をアプリのロジックに混ぜちゃっていることにすら気づかないことが多い
- e.g.
if (!user.isAdmin) return; // ここが認可ロジック runAdminProcess();
- 始めは手軽&気軽なので、あまり意識せずに組み込んでしまう
- e.g.
- あとから認可が変わって複雑になってくる
- e.g.
if (!user.isAdmin || (!resource.isOwner(user) && resource.dueDate < now())) return; runAdminProcess();
- e.g.
- 複数箇所で似たような認可(上であれば
!user.isAdmin || (!resource.isOwner(user) && resource.dueDate < now())
)をやり始める
このままでは認可を関心を分離することができず、複雑性や冗長性が上がり、変更容易性が下がります。
関心を分離した組み込み方
認可の関心を分離させるのは難しいのですが、ここで認可三大要素がポイントとなります。
- 「誰が」リクエストしているか(actor)
- 「何を」しようとしているのか(action)
- 「何に」しようとしているのか(resource)
このactor, action, resourceを使ってアプリ側のロジックとの関心の分離を試みます。
認可のインターフェイス(What Interface To Use For Our Authorization API
)
認可において大事なフェーズが2つあって、それが Enforcement(認可の適用) と Decision(認可の判断) になります。この2つの境界となるのが認可のインターフェイスです。言ってる意味がわかりづらいですが下図のイメージ。
- Enforcement(認可の適用)
- Decision(認可の判断)によって、アプリでどうふるまうか
- e.g.
- 403を返す
- データ取得時にフィルター(e.g. WHERE句)を適用する
- e.g.
- Decision(認可の判断)によって、アプリでどうふるまうか
- Decision(認可の判断)
- actor, action, resourceによって認可の判断を下す
まだわかりづらいと思うので、上の方で出てきたコード例で考えてみます。
例えば以下のコードの場合、if文の結果(認可の判断)によって「処理を継続するか」「returnするか」ということが適用(Enforce)されているのです。
if (!user.isAdmin) return; // ここが認可ロジック
runAdminProcess();
「isAdmin だから許可」というのが認可の判断(Decision)です。この、認可の判断(Decision)と認可の適用(Enforcement)の間にインターフェイスを設けることができます。
そうすることで、例えばAuthorizerというServiceがあったとしたら、以下のようなコードが書けるようになります。
if (!authorizer.isAllowed(user, "write", resource)) return;
runAdminProcess();
user
が、 resource
に対して、 write
できるかどうか(Decision)がこれで共有できることがわかります。よって、別の場所でも authorizer.isAllowed(user, "write", resource)
が呼べるようになるのです。(e.g. 403を返すのか、WHERE句を拡張するのか、など)
Decision(認可の判断)の実装方法(Options for Implementing Authorization Decisions)
ではDecision(認可の判断)はどのように実装できるのでしょうか?Decisionを下すために必要なものは以下2つ:
- Authorization Data(認可データ)
- アクセスコントロール用のデータ(e.g. AliceはAcme Organizationのメンバー)
- Authorization Logic(認可ロジック)
- あるactorが、あるresourceに対してactionを実行できるかどうかのルール(e.g. Organizationのメンバーであれば、Repositoryの参照が可能)
そして実装のアプローチは大きく3つあります。
- Decentralized Authorization(分散型)
- Centralized Authorization(集約型)
- Hybrid Authorization(ハイブリッド型)
Decentralized Authorization(分散型)
多くの場合とる方法がこの分散型になります。方針としては認可データもロジックもそれぞれのサービス内で実装する方法です。
- メリット
- 一番簡単に実装できるし、そのおかげで開発体験も良い
- データがアプリ内から容易にアクセスできる(これはかなり大きなメリット)
- サービスの数が少ないときには有効
補足
デメリットについては特にAuthorization Academyでは明示的に述べられていませんが、メリットの逆でサービスの数が多くなったときにスケールしづらくなっていきます
Centralized Authorization(集約型)
すべてのサービスのDecisionをCentralizedサービスに委譲
- メリット
- 一箇所で認可のルール(Policy)を集約できる
- 複数のサービスが同じデータを使ってDecisionしてもらえる
- デメリット
- 認可データの置き場所が難しい
- 各サービス内に置いておく場合、冗長性が減るがサービス間の結合度が上がってしまう
- 認可サービスに同期する場合、同期のコストが高い(信頼性も)
- 認可データの置き場所が難しい
Hybrid Authorization(ハイブリッド型)
それぞれのサービスでDecisionができるようにして、そのDecisionのAPIも公開して、それぞれのサービス間で必要に応じてDecisionを委譲するやり方
- メリット
- DecentralizedとCentralizedの中間でバランスがとりやすい
- デメリット
- 通信量が多くなるのでパフォーマンスが問題になりやすい(gRPCのようなプロトコルを推奨)
- Decisionのインターフェイスをある程度サービス間で一貫性を持たせる必要がある(そうしないと使い勝手が悪い)
次はどうすれば?
まとめに関しては冒頭の「いきなりまとめ」の通りです。
本記事ではAuthorization Academyの中のII章にあたる「What is Authorization?」について概要をまとめました。認可についてや、認可のインターフェイス、アーキテクチャに関する考え方がこの記事でつかめるかと思います(原文を読むのが一番おすすめです)。
次はどうするかと言うと、Authorization AcademyのIII章以降の「Authorization Modeling」が重要になってきます。認可モデルの設計です。本記事の内容はあくまで技術的な側面が強かったのですが、認可モデルをしっかり設計しないと、いかにテクニカルにすばらしくても破綻します。
認可モデルを考える際にRBAC(Role-Based Access Control)、ABAC(Attribute-Based Access Control)、さらにReBAC(Relation-Based Access Control)という概念が登場します。このあたりに関してもしっかり抑えていくことをおすすめします。(要は「Authorization Academyを読んでみましょう」ってことです!)
ということでIII章は以下です!ぜひ読んでみましょう🚀
「SHElikes(シーライクス)」を運営するSHEの開発チームがお送りするテックブログです。私たちは社会的不均衡の解決を目指すインパクトスタートアップです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 採用情報 -> bit.ly/3XxywnD
Discussion