はじめに
本記事は any Advent Calendar #2 「マルチテナントSaaSにおけるエンジニアリング大全」 Day6 の記事です。弊社anyのアドベントカレンダーをひとつ丸ごと占有して、ひとりアドベントカレンダーとして、筆者の「マルチテナントSaaSのエンジニアリング」への経験をすべてアウトプットしていくカレンダーです。
今回はアプリケーションの認可処理に関するプラクティスについて紹介していきます。 マルチテナントに限らず、アプリケーションの権限にかかわる諸問題はOso Academyの記事がオススメです。この記事でも、Oso Academyの記事を拝借しながら解説を進めていきます。
認可とは
認可(Authorization)とは、アプリケーション内で「誰が何を実行できるのか」を制御するメカニズムです。
認証はユーザーが「本当に本人であるか」を確認するプロセスです。例えば、メールアドレスとパスワード、OAuth、SAMLなどで身元を確認することを指します。一方、認可は認証を通過した後に「何ができるか」を決定します。
Oso Academyの表現を借りれば、「もし認証が玄関ドアなら、認可は一度中に入った後、どのドアを開けることができるかを制御する」ということになります。
認可の決定には3つの要素が関わります。この区分は重要なので覚えておくと良いでしょう👍
- Actor(主体): リクエストを発行するユーザーまたはエンティティ
- Action(アクション): 実行しようとしている操作(作成、読取、更新、削除)
- Resource(リソース): 操作の対象となるリソース
これらの3要素の組み合わせで「このユーザーはこのリソースに対してこの操作を実行できるか」という判定が成立します。
認可の実装パターン
認可の実装方法にはいくつかのパターンが存在します。Oso Academyの資料を参考に、代表的な3つのパターンを紹介します。今後の説明のためにもまずは座学的に解説をさせてください。
RBAC(Role-Based Access Control)
RBACは、権限を名前付きのロール(役割)にグループ化し、それらをユーザーに割り当てるモデルです。例えば「管理者」「メンバー」「閲覧者」といったロールを定義し、ユーザーはいずれかのロールに属することで、そのロールに紐付いた権限を得ます。
シンプルで理解しやすいため、実装としては容易である反面、ロール数が増加すると管理が複雑になる(ロール爆発)デメリットがあります。
ABAC(Attribute-Based Access Control)
ABACはロールではなく、ユーザー属性(部門、雇用形態、認可レベル等)、リソース属性(所有者、公開設定等)、環境属性(リクエスト時刻、IPアドレス等)といった複数の属性情報を組み合わせて、アクセス判定を行うモデルです。
ロール爆発を防ぎながら、時間的制限やロケーション制限など、コンテキストを考慮したアクセス制御が可能になるため、機密データを扱う金融機関や医療機関での採用が多いです。一方で、実装が複雑になりやすく、ルール設計に高度な知識が必要という課題があります。
ReBAC(Relationship-Based Access Control)
ReBAC(関係ベースアクセス制御)は、ユーザーとリソース間の関係性に基づいて権限を決定するモデルです。例えば「Aliceはissue #412のオーナーである」「Bobはエンジニアチームのメンバーである」といった関係性をベースに、アクセス判定を行います。
アプリケーションの既存データ構造を活用でき、チームやグループといった自然な関係性を権限に反映できるため、RBACとABACの中間的なアプローチとして注目されています。一方で、複雑な関係性の管理が必要になり、グラフベースのデータ構造が必要になる可能性がある点が課題です。
Qastにおける実例
さてここまでが前座で、マルチテナントSaaSにおいては、この認可の処理が非常に複雑になりがちです。いずれの方式もトレードオフの関係性であり、正解・不正解があるわけではありません。ちなみに弊社QastではいわゆるRBAC(Role-Based Access Control)の仕組みを利用した認可をおこなっています。
具体的にはユーザは「管理者」や「リーダー」といったActor(主体)のいずれかに属し、その権限に応じた操作が可能になるという流れです。非常にシンプルかつ一般的なモデルではありますが、その分細かい制御が効かないため、柔軟性がより必要な場面がどうしても発生してしまいます。
アプリケーション実装に関わる話
より実装に近い点での認可についても紹介していきましょう。 Oso Academyでは、その認可の決定を要求し、その結果に基づいて行動することをEnforcementと定義しています。アプリケーションの実装においては多段階でそのEnforcementが必要になることを認識せねばなりません。
https://www.osohq.com/academy/authorization-enforcement
認可エンフォースメントの実装層
上図で示されるように、認可のエンフォースメントは単一の層では完結せず、アプリケーションの複数の段階で実施される必要があります。具体的には以下の4つの層で行われます:
- (青)リクエスト層:HTTPメソッドやパスなどのメタデータに基づく初期判定
- (緑)ビジネスロジック層:リソースレベルでの最も重要なエンフォースメント。アプリケーションのコンテキストを活用した粒度の高い判定
- (赤)データ層:データベースクエリに認可フィルタを適用し、権限のないリソースをそもそも取得しない
- プレゼンテーション層:クライアント側での操作制限。セキュリティのためではなく、UXの向上が目的になる
重要なのは、認可の判断と結果に基づいた行動を分離することとされています。
アプリケーションのコードレベルでは「認可の判断」の関数で認可の判定を行い、その結果に応じて適切に処理を分岐させます。より平たく言えば インターフェースが統一されていること、またbooleanを返却し、返却先で処理を分岐できることがポイントになります。
// 呼び出し元。アプリケーションの認可ロジックをインターフェースとして統一
// `actor`, `action`, `resource` => boolean のインターフェースとする
function isAllowed<T>(actor: Actor, action: string, resource: T): boolean { }
// 利用する側で処理を分岐する例
if (!isAllowed(actor, "update", post)) {
// 結果に基づく処理は呼び出し元が定義する(もちろん例外を投げない処理でもかまわない)
throw new UnauthorizedError("この投稿を更新する権限がありません");
}
// 認可が通った場合に処理を実行する
await updatePost(post);
実装においては、Domain-Driven Designなどのアーキテクチャパターンを前提にしながら、ユースケース層、ドメイン層、クエリサービス層など、レイヤーごとに適切な認可ロジックの配置を検討する必要があります。 下記のログラスさんの記事では実務に沿った内容での記載があり、非常にオススメです。
まとめ
さて、マルチテナントSaaSにおけるアプリケーション認可という点で解説してきましたが、本質的にはマルチテナント特有の問題というわけではないかもしれません。しかしながら、マルチテナントSaaSでは、テナントごとに認可の複雑性が異なるため、それに引きずられる形で全体が複雑になりがちです。しかも権限自体の複雑性は、プロダクトのUI/UXとしての分かりにくさにつながることにもなります。
シンプルなRBACから始めたとしても、要件の複雑化に伴い、より柔軟な制御が必要になっていくのは自然なことです。重要なのは、その時点での正解を選ぶことではなく、認可の判断と実行を分離し、複数のレイヤーでのエンフォースメントを設計することで、複雑性をカプセル化すること でしょう。
Oso Academyの記事やログラスさんのテックブログで紹介されているように、既に多くの知見が共有されているため、これらを参考にしながら段階的にシステムを進化させていくことをお勧めします!


