🔐

認可のアーキテクチャに関する考察(Authorization Academy IIを読んで)

2023/08/14に公開

みなさま、認可の設計に苦しんでいるでしょうか?私は苦しんでいます。苦しまなかった瞬間などありません。昔「アプリケーションにおける権限設計の課題」を執筆しましたが、あれから3年以上が経ちます。

https://kenfdev.hateblo.jp/entry/2020/01/13/115032

当時は認可の設計に関する情報がうまくまとまっている記事などほとんど無く、調べに調べて得たナレッジを書き記したのが上記の記事です。3年以上経ちますが、苦悩が今も特に変わっていないことが驚きです。

ただし、世の中的には認可のライブラリであったりサービスというのは少しずつ増えてきている印象があります(Auth0の OpenFGA であったりOsoの Oso Cloud 、Asertoの Topaz )。

認可の設計に関する記事も少しずつ増えている印象があり、その中でも本記事で紹介したいのがAuthorization Academyです。

https://www.osohq.com/academy

これは認可サービスである Oso Cloud やOSSのライブラリ oso を提供しているOsoが出している認可に関するナレッジがシェアされている 神記事 です。この記事が昔存在していたら、どれだけ自分が救われただろうか、と思えるくらいにすばらしくまとまっている記事です。

https://github.com/osohq/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
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 など)
    • 何を
      • HTTPリクエストの種類が確認できるので、 GET しようとしているかどうかなどが特定できる
    • 何に
      • アクセス先のURLのパス(e.g. www.gitclub.dev/acme/anvil
  • どのような認可ができる?
    • ルーティングレベルの認可ならできる
      • e.g. alice@acme.org は /acme/anvil に GET できる?

補足
AWSを使ったことのある人はわかるかもしれませんが、API GatewayにはAuthorizerという仕組みがあり、そこでJWTの中身に応じて(例えば roleemail など)認可を適用できます

WebアプリのRouter部分

これはWebアプリのいわゆるルーティング部分にあたります。例えばNode.jsのExpressで言うExpress Routerとか、渡ってきたリクエストを適切なハンドラーに委譲するレイヤーです。

  • 認可三大要素の確認
    • 誰が
      • この時点ではRouterのミドルウェアがデータベースからUserに関連する情報を取得してUserオブジェクトみたいなものを作ることができる
    • 何を
      • この時点ではまだHTTPリクエストの種類が確認できる程度(e.g. GETとか)
    • 何に
      • 同じく、リクエストされたパスがわかる程度
  • どのような認可ができる?
    • やろうと思えばいくらでもできる(DBへのアクセスもできるので)
    • ただし、Controller以降の処理でも同じようなことをする必要が出てくることが多いので、あまりこの時点では追加の情報は取得せずに認可を行う
    • 例えば、Userオブジェクトに isAdmin: true があるなら、 /admin 関連のパスへのアクセスを許可するかどうか認可できる

補足
この記事では詳しく触れませんが、Roleによって認可を適用する(Role-Based Access Control)などは、このレイヤーで「 admin Roleを持っているから許可する」のようなことをよくやります

WebアプリのController部分

ここがアプリケーションのコアな部分にあたります。

  • 前提
    • 前処理でRouterがControllerの view_repository メソッドにマップしたと仮定
  • 認可三大要素の確認
    • 誰が
      • Routerの段階でUserオブジェクトが既にあるので「誰」なのかはわかっている
    • 何を
      • Repositoryの情報を参照
    • 何に
      • 対象となるRepository(e.g. acme/anvil
  • どのような認可ができる?
    • ここまで来るとすべての情報がそろっているので認可可能
      • 例: 「anvil Repositoryは acme Organizationが持っていて、 alice@acme.orgacme に所属している」ので「許可」されることになる

補足
Controllerと言っているものの、レイヤーとしてはRouterが呼び出す先のアプリケーションのコア部分のレイヤーのことです。主にビジネスロジックが関わる部分になります

データアクセスの部分

アプリケーションにおいてデータ取得をするレイヤーにおいても認可をかけることができます。

  • 認可三大要素の確認
    • 誰が
      • Routerの段階でUserオブジェクトが既にあるので「誰」なのかはわかっている
    • 何を
      • (RDBであれば)SQLのSELECTを実行
    • 何に
      • Repositoryテーブルから、対象となるRepository(e.g. acme/anvil)を絞り込んだもの
  • どのような認可ができる?
    • 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にハードコードしてしまっている点
        • 認可ロジックとビジネスロジックの分離ができていません

上記で認可の情報を「ハードコードしてしまっている」点については「アプリケーションにおける権限設計の課題」の「権限実装のアプローチ」でも紹介しています。「どこがハードコートなの?」と思う人はぜひ読んでみてください。

https://kenfdev.hateblo.jp/entry/2020/01/13/115032#権限実装のアプローチ

またOsoにはData Filterという仕組みがあり、これがなかなかおもしろいので興味がある人はこちらも深掘りしてみると良いです。

https://docs.osohq.com/node/guides/data_filtering.html

補足
簡単に書いていますが、このレイヤーでキレイに認可を適用するのはかなり難しいです。例えば上記でSQLに認可を適用していますが、「どのようなSQLなのか?」、「どんなテーブルをどんな名前でJOINしているのか?」、「カラム名はSQL内で変わっていないか?」など、不確定要素が多いからです。Prismaなど、型補完の効くORMなどと併用することである程度現実的に適用できますが、ORMの採用はそれはそれでデメリットもあったりするので、筆者はこのあたりのグッドプラクティスはまだ模索中です

以上で、「認可はどこでどのように適用できるのか」というのを各レイヤーの観点で理解できます。

認可のアプリへの組み込み方(Adding Authorization to an Application)

やってしまいがちな組み込み方

  • 認可を始めから意識して作れることは少ない
  • 認可をアプリのロジックに混ぜちゃっていることにすら気づかないことが多い
    • e.g.
      if (!user.isAdmin) return; // ここが認可ロジック
      
      runAdminProcess();
      
    • 始めは手軽&気軽なので、あまり意識せずに組み込んでしまう
  • あとから認可が変わって複雑になってくる
    • e.g.
      if (!user.isAdmin || (!resource.isOwner(user) && resource.dueDate < now())) return;
      
      runAdminProcess();
      
  • 複数箇所で似たような認可(上であれば !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句)を適用する
  • 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章は以下です!ぜひ読んでみましょう🚀

https://www.osohq.com/academy/role-based-access-control-rbac

SHE Tech Blog

Discussion