webアプリケーションにおける認可の実装事例
この記事は、Nowcast Advent Calendar 2025 の4日目の記事です。
導入
こんにちは、ナウキャストでソフトウェアエンジニア・データサイエンティストをやっている隅田です。直近ではDataLens店舗開発というwebアプリケーションの開発をメインに行っています。コア機能となる物件情報の自動抽出機能の大部分は生成AIにより実現されています。これ以外にも、生成AIの力で実現できた高度な機能の事例が社内に数多く存在します。あらゆるエンジニアが生成AIを(自身の業務効率化にとどまらず)ユーザーへの新たな価値提供のために活用していく世界になっていくのでしょう。すると、LLMOpsを始め様々な新しい論点が生じます。例えば、最近の登壇ではLLMの構造化出力の精度をどのように管理すべきについて話しました。
ところで最近、カジュアル面談でナウキャストにはデータ・AIのイメージしかないという話をよく頂いており、ソフトウェアエンジニアリングもしっかりやってるよ!という発信もしていかねばと思っています。そこで、今回はあえてデータ・AIに関連しないテーマで記事を書きます。テーマは認可の実装についてです。
認可の実装は難しい
認可(以下、権限管理やアクセス制御も同義)とは、誰が何にどのような操作をどのような条件下で行ってよいかという判定を行う処理のことです。色々と便利なので少し用語を整理しておくと、
| 認可文脈での用語 | |
|---|---|
| 誰が | Principal / Subject |
| 何に | Resource / Object |
| どのような操作を | Action |
| どのような条件下で | Context / Condition |
となります。また、以下ではRole Based Access Control (RBAC)やAttribute Based Access Control (ABAC)については既知とします。このあたりの基本的な知識を学びたい方はAuthorization Academyが良い資料です。
さて、認可の実装はwebアプリケーション開発において多くの場合避けて通れませんが、正しく実装する難易度が相対的に高い領域と思われます。実際、2021年版のOWASP Top 10でもBroken Access ControlがNo.1に位置しています(まだ確定ではありませんが、2025版でも引き続きNo.1になる見込みです)。
では、どのように認可の実装をしていけば良いのでしょうか?もちろん、webアプリケーションの規模、用途、開発体制など様々な要因により最適解は決まってくるので一概には言えませんが、直近でこのテーマについて調査や設計判断を行いましたので、事例として参考にして頂ければと思い、本記事のトピックにすることを決めました。
設計の指針
認可の設計指針としてまず参照したのが以下のガイドラインです。
以下で今回の設計判断に特に関わる部分を、私見も交えつつピックアップしたいと思います。
認可判定ロジックを一元化する
認可判定のロジックが一元化されておらず分散していると以下のような問題が発生します。
- 実装難易度が高い仕組みの再発明が繰り返されるため、開発生産性が低下する
- ビジネスロジックと認可ロジックが混在し、コードの見通しが悪くなる
- 個別の認可判定が正しく実装されている保証がない
- アプリケーションの認可判定が全体として整合的である保証がない
そのため、認可判定を担うロジックは各種ビジネスロジックとは切り離し、一箇所に集約されているべきです。
ABACを前提に設計する
RBACは仕組みとしてシンプルでとっつきやすいため、多くのアプリケーションで採用されていると思われます。しかし、RBACは柔軟性の観点で限界があります。例えば、共有機能で指定したprincipalに指定したresourceのみを閲覧可能にするとかあるタイプのresourceはそのresourceを作成したprincipalのみ削除可能のようにprincipal/resource単位のきめ細かいアクセス制御を行いたい場合、RBACで自然と実装するのは難しくなります。アプリケーションがまだシンプルなフェーズにRBACで実装を行ったものの、後からきめ細かい認可ロジックが必要となってしまった、というのはよくあるケースのようです。こういうケースに無理にRBACで乗り切ろうとして大量に細かいロールを作成してしまう現象をRole Explosionと言ったりします。今回行った設計検討の時点ではRBACで実装しづらい具体的なユースケースが既に見えていましたが、仮にそれがなかったとしても拡張性の観点からRBACに可能性を閉じて良いかは考慮すべきポイントです。
ABACはRBACを概念的に含んでいるので、RBACをメインのモデルとして使用しつつも、柔軟なアクセス制御を実現することが出来ます。したがって、一定以上の規模のアプリケーションであればABACの採用を検討する価値があると思います。より詳細な議論についてはこちらが参考になります。
実装の指針
上記の指針を踏まえると、ABACを実装する何らかの認可エンジンが必要という結論に至ります。これをどのように実装するべきでしょうか。
最初の判断軸として、可能な限り自前実装を避けたいという点が挙げられます。サービスが急成長しており新規機能の要望が大量にある中で、ABACの認可エンジンをスクラッチから実装する余裕はありません。したがって何らかの既成ツールを利用したくなります。
次の判断軸として、導入ハードルの低さが挙げられます。オープンソースで手軽に利用可能なツールや、既に利用しているAWS上で完結可能なツールが望ましいです。
これらの基準の下、次節で説明するCedarを利用することに決めました。
Cedar
Cedarは認可判定に特化したシンプルかつ高速なDSLです。AWSがオープンソースで開発しており、CedarのマネージドサービスがAWS Verified Permissionになります。イメージとしては、AWS IAMが汎用化されて任意のアプリケーションでカスタムのポリシーを書けるようなツールです。以下にCedarで書かれたポリシーを例として示します。
permit(
principal == User::"alice",
action == Action::"update",
resource == Photo::"VacationPhoto94.jpg"
when {
context.mfa_passed == true
};
ここで、User,Action,Photoはクラス、alice, update, VacationPhoto94.jpgは識別子です。どのようなクラスが存在し、それらがどのような属性値を持っているかは自由に定義することが出来ます。上記のポリシーの意味合いを英語で述べると、Permit the user named alice to update the photo named VacationPhoto94.jpg when MFA passedといった内容で、自然言語による表現と非常に近いことが分かります。
Cedarでは集合や論理演算もサポートされているので、以下のようなポリシーも書くことが出来ます。
permit(
principal in Role::"PhotoEditor",
action in [Action::"read", Action::"update"],
resource == Photo::"VacationPhoto94.jpg"
) when {
resource.accessLevel == "public" && principal.location == "USA"
};
これも英語で意味合いを述べるとPermit any user who has the PhotoEditor role to read or update the photo “VacationPhoto94.jpg”, when the photo's access level is public and the user's location is USA.といった内容です。
※上記はあくまで分かりやすい例として書いており、実際の識別子としてはUUIDを用いますし、ポリシーももう少し抽象度を上げて書きます。
これらの例からも分かるように、アプリケーションのドメイン構造を反映させたポリシーを自由度高く記述していくことが出来ます。認可判定を行う際には、具体的なprincipal/action/resource/contextの組をポリシーの集合と照らし合わせ、allowもしくはdenyの結果を得ます。実際に動かしてみたい方は、こちらのplaygroundで手軽に動かして遊んでみましょう。また、ポリシーとして書ける内容は上記以外にもたくさんありますので、興味がある方はこちらのチュートリアルがサラッと読めてオススメです。
Dependency Inversion
初期的な検証の結果、Cedarにより必要な認可ロジックは問題なく実装できることが分かりました。一方で、比較的新しい技術であり採用実績が少ないことも確かなので、検証段階では見えていないリスクも想定しておく必要があります。将来的にCedar以外の方法に切り替える必要が生じた場合に移行コストが高くつくのは避けたい事態です。
これについては、基本的な開発規約としてDependency Inversionを導入しているため問題にならないと考えています。以下、実際のコードを示します(分かりやすくするため、少し改変しています)。
from typing import Protocol
from app.domain.shared.authorization import AuthPrimitive, Principal, Resource # これらの定義は省略
# 認可要求のインタフェース
class AuthorizationRequest(Protocol):
@property
def principal(self) -> Principal: ...
@property
def action(self) -> str: ...
@property
def resource(self) -> Resource: ...
@property
def context(self) -> dict[str, AuthPrimitive]: ...
# 認可エンジンのインタフェース
class AuthorizationEngine(Protocol):
# allowならTrue、denyならFalseを返す
def authorize(self, request: AuthorizationRequest) -> bool:
...
# AuthorizationEngineのCedarによる実装
class CedarAuthorizationEngine:
def authorize(self, request: AuthorizationRequest) -> bool:
# Cedarを動かすための諸々のコード
アプリケーションコードもテストコードも全てAuthorizationEngineにのみ依存しており、実行時にAuthorizationEngineの実装クラスを指定するようになっています。したがって、将来的にCedarとは異なる方式を使用する必要が出た際には、AuthorizationEngineの新しい実装クラスを開発し、それに差し替えるだけで良いことになります。
おわりに
認可の設計・実装を行う上では、本記事で触れた以外にも様々な重要な論点があります。例えば、こうした認可判定をどの部分で強制するか(i.e. Policy Enforcement Pointをどう実装するか)、認可処理の監視をどのように行うべきか、Cedar以外にどのようなツールがありそれらのメリデメは何なのか、などなど。別の機会にこうした論点について紹介できればと思います!本記事が参考になれば幸いです。
Discussion