FastAPIで実装するRBAC/ABAC権限管理入門:ロール・属性・ポリシーで安全に認可を設計する実務ガイド
要約
- 認証は「その人が誰か」を確かめる仕組みで、認可は「その人が何をしてよいか」を決める仕組みです。FastAPIの実務では、この認可設計がアプリの安全性と保守性を大きく左右します。
- 小さなアプリではRBAC(Role-Based Access Control、ロールベース認可)から始めるのが現実的です。
admin、editor、viewerのような役割で権限をまとめると、実装と運用が分かりやすくなります。 - ただし、SaaSやマルチテナント環境では、ロールだけでは表現しきれない条件が増えます。そこでABAC(Attribute-Based Access Control、属性ベース認可)を組み合わせ、「所属テナント」「所有者かどうか」「公開状態」「プラン」などの属性で判断する設計が重要になります。
- FastAPIでは、
DependsとSecurityを使い、認証済みユーザー、テナント、対象リソースを依存関数で解決し、その上でポリシー関数に認可判断を集約すると、見通しが良くなります。 - 本記事では、RBACとABACの違い、FastAPIでの実装パターン、マルチテナントとの組み合わせ、監査ログとの連携、テスト戦略までを、段階的に丁寧に整理していきます。
誰が読んで得をするか
個人開発・学習者さん
ログイン機能までは作れたものの、「管理者だけが編集できる」「本人だけが退会できる」といった認可をどう書けばよいか迷っている方に向いています。
最初は if current_user.role == "admin" のような書き方でも動きますが、少し機能が増えると散らかりやすくなります。この記事では、そこから一歩整理された形へ進む道筋をお届けします。
小規模チームのバックエンドエンジニアさん
FastAPIで管理画面や社内ツール、SaaSのAPIを作っていて、「ロール判定が各所に散っていて危ない」「権限変更の影響範囲が見えない」と感じている方に向いています。
RBACの基本を押さえつつ、ABACをどこまで入れるべきか、サービス層と依存関数をどう分けるべきか、といった実務的な悩みに答えやすい構成です。
SaaS開発チーム・スタートアップの皆さま
マルチテナント環境で、テナント内ロール、所有者制御、プランによる機能制限、監査ログなどが絡み、「単なるadmin判定では足りない」と感じているチームに向いています。
認可ルールを場当たり的に増やすと、後からとても直しづらくなります。早い段階で「どこに判断を置くか」を整理しておくと、将来の複雑化にも耐えやすくなります。
アクセシビリティ評価
- 最初に全体像を示し、その後に「RBACの基本」「ABACの導入」「FastAPIでの依存関数」「ポリシー関数」「マルチテナント」「テストと監査」という順序で進めています。読みたい章だけ拾っても理解しやすい構成です。
- 専門用語は初出で短く補足し、その後は同じ表現で統一しています。認知負荷を下げることを意識しました。
- コード例は短めに分け、1つのブロックで1つの責務だけを見せています。視線が迷いにくいようにしています。
- 章ごとに要点が分かるよう見出しを細かく置いています。目標レベルはAA相当です。
1. 認可を後回しにすると、なぜ危ないのか
FastAPIに限らず、アプリケーションを作り始めた初期は、まず「ログインできること」が大きな節目になります。
けれど、本当に重要なのはその次で、「ログインした人がどこまでできるか」をどう制御するかです。
ありがちな初期実装は、次のような形です。
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="forbidden")
最初はこれで十分に見えるのですが、機能が増えると次のような問題が出てきます。
- ルーターごとに判定がバラバラになる
admin以外のロールが増えたときに修正箇所が多すぎる- 「本人だけ編集可」「同じテナントの管理者だけ可」などの条件が混ざる
- フロントとバックエンドで認識している権限名がずれる
- 監査ログに「なぜ拒否したか」を残しづらい
つまり、認可は単なる if 文ではなく、設計の対象なのです。
ここを整えると、セキュリティだけでなく、コードの見通しと変更しやすさも大きく改善します。
2. RBACとは何か:ロールでまとめて管理する
RBACは、Role-Based Access Control の略で、「役割」に応じて権限を決める考え方です。
たとえば、記事管理アプリなら次のように考えられます。
admin:すべてできるeditor:記事の作成・編集・公開ができるviewer:閲覧だけできる
この方式の良いところは、人間にとって分かりやすいことです。
「この人はeditorです」と言えば、チームでも説明しやすく、UIでも表示しやすいです。
2.1 RBACが向いているケース
RBACは、次のような場面で特に強いです。
- 社内管理画面
- 小〜中規模の管理API
- 権限パターンが比較的固定されているアプリ
- テナントごとの役割が明確なSaaS
最初に導入する認可方式として、とても扱いやすいです。
2.2 RBACだけでは足りなくなる場面
ただし、実務では次のような条件がよく出てきます。
editorでも自分が作成した記事しか編集できないviewerでも公開済みコンテンツだけは見られる- 同じ
adminでも、所属していないテナントのデータは見られない - プランが無料のテナントでは一部機能が使えない
このような「ロール以外の属性」による判定が増えると、RBACだけでは表現しづらくなります。
そこでABACが登場します。
3. ABACとは何か:属性で細かく判断する
ABACは、Attribute-Based Access Control の略で、「属性」を使って認可を判断する考え方です。
ここでいう属性とは、たとえば次のような情報です。
- ユーザー属性
user_id,role,department,plan
- リソース属性
owner_id,tenant_id,status,visibility
- 環境属性
- 時刻、IPアドレス、アクセス元環境
- テナント属性
- 契約プラン、オプション、凍結状態
たとえば、次のような判断はABACの典型です。
- 「記事の
owner_idが現在ユーザーのuser_idと一致するなら編集可」 - 「テナントの
planがpro以上ならエクスポート可」 - 「リソースが
publicなら未ログインでも閲覧可」
つまりABACは、「この人のロールは何か」だけではなく、「この人と対象の関係はどうか」まで見て判断する仕組みです。
3.1 RBACとABACは対立ではなく、組み合わせるもの
実務では、RBACとABACはどちらか一方だけを使うというより、組み合わせることが多いです。
たとえば、
- RBACで大枠を決める
viewerは更新系APIを触れない
- ABACで細かい条件を決める
editorでも自分の所属テナントのプロジェクトだけ編集可
この二段構えにすると、理解しやすさと柔軟性の両方を取りやすくなります。
4. FastAPIでの基本方針:認可は依存関数とポリシー関数に寄せる
FastAPIでは、認可ロジックをルーターに直書きするより、依存関数やポリシー関数へ寄せる方が見通しが良くなります。
役割を分けると、次のようになります。
- 認証依存関数
- 現在ユーザーを取得する
- テナント依存関数
- 現在の
tenant_idを解決する
- 現在の
- リソース依存関数
- 対象プロジェクトや記事を取得する
- ポリシー関数
- 「このユーザーはこの操作をしてよいか」を判断する
この分け方にしておくと、ルーターでは「何を使って判定しているか」が読みやすくなりますし、テストも書きやすくなります。
5. ベースとなるユーザー・テナント・ロールのモデル
まずは概念が分かりやすい最小モデルを用意します。
# app/models/auth.py
from pydantic import BaseModel
class CurrentUser(BaseModel):
user_id: int
email: str
global_role: str | None = None
tenant_ids: list[int] = []
active_tenant_id: int | None = None
ここでは、シンプルに次の前提にしています。
global_roleは全体管理者のような全局権限tenant_idsは所属テナント一覧active_tenant_idは現在選択中のテナント
さらに、テナントごとのロールは別で管理します。
# app/models/membership.py
from pydantic import BaseModel
from typing import Literal
TenantRole = Literal["owner", "admin", "member", "viewer"]
class TenantMembership(BaseModel):
user_id: int
tenant_id: int
role: TenantRole
これで、「全体管理者」と「テナント内の役割」を分けて考えられるようになります。
6. 認証済みユーザーとテナントを依存関数で解決する
6.1 現在ユーザーを返す
実際にはJWTを使うことが多いですが、ここでは説明用に簡略化しています。
# app/deps/auth.py
from fastapi import Depends
from app.models.auth import CurrentUser
def get_current_user() -> CurrentUser:
return CurrentUser(
user_id=1,
email="hanako@example.com",
global_role=None,
tenant_ids=[10, 20],
active_tenant_id=10,
)
6.2 現在の tenant_id を確定する
# app/deps/tenant.py
from fastapi import Depends, Header, HTTPException, status
from app.deps.auth import get_current_user
from app.models.auth import CurrentUser
def get_current_tenant_id(
current_user: CurrentUser = Depends(get_current_user),
x_tenant_id: int | None = Header(default=None),
) -> int:
tenant_id = x_tenant_id or current_user.active_tenant_id
if tenant_id is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="tenant is not selected",
)
if tenant_id not in current_user.tenant_ids:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="tenant access is forbidden",
)
return tenant_id
ここで大切なのは、「ヘッダやパスで指定された tenant_id をそのまま信じない」ことです。
必ず、そのユーザーが本当に所属しているかを確認します。
7. RBACの最小実装:ロール判定を依存関数化する
まずは、RBACだけで足りるシンプルな場面から始めましょう。
たとえば、テナント管理者だけがメンバー追加できるようにしたい場合です。
7.1 テナント内ロールを取得する関数
# app/deps/membership.py
from fastapi import Depends, HTTPException, status
from app.deps.auth import get_current_user
from app.deps.tenant import get_current_tenant_id
from app.models.auth import CurrentUser
from app.models.membership import TenantMembership
def get_current_membership(
current_user: CurrentUser = Depends(get_current_user),
tenant_id: int = Depends(get_current_tenant_id),
) -> TenantMembership:
# 実際はDBから引く
if current_user.user_id == 1 and tenant_id == 10:
return TenantMembership(user_id=1, tenant_id=10, role="admin")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="membership not found",
)
7.2 特定ロールを要求する依存関数
# app/deps/permissions.py
from fastapi import Depends, HTTPException, status
from app.deps.membership import get_current_membership
from app.models.membership import TenantMembership
def require_tenant_admin(
membership: TenantMembership = Depends(get_current_membership),
) -> TenantMembership:
if membership.role not in {"owner", "admin"}:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="admin role required",
)
return membership
7.3 ルーターで使う
# app/api/v1/routers/members.py
from fastapi import APIRouter, Depends
from app.deps.permissions import require_tenant_admin
router = APIRouter(prefix="/members", tags=["members"])
@router.post("")
def invite_member(
membership = Depends(require_tenant_admin),
):
return {"status": "invited", "tenant_id": membership.tenant_id}
このようにしておくと、ルーターでは「管理者権限が必要」という意図が読み取りやすくなります。
8. ABACの基本実装:所有者やリソース属性で判断する
次は、RBACだけでは足りないケースです。
たとえば「同じテナント内のメンバーでも、自分が作成したプロジェクトしか編集できない」ような条件を考えます。
8.1 リソースモデルの例
# app/models/project.py
from pydantic import BaseModel
from typing import Literal
class Project(BaseModel):
id: int
tenant_id: int
owner_id: int
name: str
visibility: Literal["private", "public"] = "private"
8.2 対象リソースを取得する依存関数
# app/deps/project.py
from fastapi import Depends, HTTPException, status
from app.deps.tenant import get_current_tenant_id
from app.models.project import Project
def get_project(project_id: int, tenant_id: int = Depends(get_current_tenant_id)) -> Project:
# 実際はDBから tenant_id 条件付きで取得
if project_id == 1 and tenant_id == 10:
return Project(id=1, tenant_id=10, owner_id=1, name="Project A", visibility="private")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="project not found",
)
ここで既に、project_id 単独ではなく tenant_id とセットで取得しているのがポイントです。
8.3 ポリシー関数で判断する
# app/policies/project_policy.py
from app.models.auth import CurrentUser
from app.models.membership import TenantMembership
from app.models.project import Project
def can_edit_project(
current_user: CurrentUser,
membership: TenantMembership,
project: Project,
) -> bool:
if membership.role in {"owner", "admin"}:
return True
if membership.role == "member" and project.owner_id == current_user.user_id:
return True
return False
8.4 依存関数として包む
# app/deps/permissions.py
from fastapi import Depends, HTTPException, status
from app.deps.auth import get_current_user
from app.deps.membership import get_current_membership
from app.deps.project import get_project
from app.policies.project_policy import can_edit_project
def require_project_edit_permission(
current_user = Depends(get_current_user),
membership = Depends(get_current_membership),
project = Depends(get_project),
):
if not can_edit_project(current_user, membership, project):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="project edit is forbidden",
)
return project
この形にすると、「判定ロジックそのもの」は純粋関数としてテストしやすく、FastAPI依存の部分は薄く保てます。
9. ABACでよく使う属性の種類
実務でよく出てくるABACの属性を整理しておくと、設計がしやすくなります。
9.1 ユーザー属性
user_idglobal_roleemail_verifiedis_activedepartmentplan
9.2 テナント属性
tenant_idplan(free / pro / enterprise)status(active / suspended)feature_flags
9.3 リソース属性
owner_idtenant_idstatus(draft / published / archived)visibility(private / internal / public)
9.4 環境属性
- アクセス時刻
- IPアドレス
- 地域
- クライアント種別
最初から全部を見る必要はありませんが、「ロール以外に何を見れば安全か」を整理するための視点として持っておくと便利です。
10. プラン制限も認可の一部として扱う
SaaSでは、機能制限がプランにひもづいていることがよくあります。
これもABACの一種として扱うと、設計がまとまりやすいです。
10.1 テナントプランに基づく判定
# app/models/tenant.py
from pydantic import BaseModel
from typing import Literal
class Tenant(BaseModel):
id: int
name: str
plan: Literal["free", "pro", "enterprise"]
# app/policies/export_policy.py
from app.models.tenant import Tenant
from app.models.membership import TenantMembership
def can_export_csv(tenant: Tenant, membership: TenantMembership) -> bool:
if tenant.plan not in {"pro", "enterprise"}:
return False
if membership.role not in {"owner", "admin", "member"}:
return False
return True
このようにすると、「ロール」と「プラン」の両方を明示的に扱えます。
あとから「enterpriseだけ監査ログの保持期間延長」などのルールが増えても、考え方を揃えやすいです。
11. ルーターに書きすぎない:ポリシーをサービス層や専用モジュールへ集める
認可を各ルーターで if 文として書き始めると、数か月後にとても読みづらくなります。
おすすめは、次のような分離です。
- ルーター
- 依存関数を並べる
- ポリシー関数
- 認可ルールを表現する
- サービス層
- 重要な業務ルールと合わせて判断する
たとえば「プロジェクト公開」は、単に編集権限があるだけでなく、「既にアーカイブ済みではない」といった業務ルールもあるかもしれません。
そうした場合、ルーターだけではなくサービス層と一緒に認可を考える方が自然です。
12. 監査ログと組み合わせる:拒否された操作も残す
前回の監査ログ設計の記事ともつながる部分ですが、認可の拒否も重要な証跡です。
とくに次のようなイベントは、監査対象にする価値があります。
- 他テナントのデータへアクセスしようとした
- 管理者権限が必要な操作を一般メンバーが試みた
- プラン制限で利用不可の機能を呼び出した
- APIキーで禁止されたエンドポイントへアクセスした
たとえば、認可失敗時に次のような監査イベントを残せます。
detail={
"reason": "role_not_allowed",
"required": ["owner", "admin"],
"actual": "viewer",
}
こうした記録があると、問い合わせ対応やセキュリティ監視がしやすくなります。
13. よくある失敗パターン
13.1 is_admin フラグだけで全てを決める
初期は便利ですが、テナント内ロールや所有者判定が入ると破綻しやすいです。
13.2 tenant_id を見ずに resource_id だけで取得する
これはマルチテナントで最も危険なミスの一つです。
必ず tenant_id を条件に含めます。
13.3 認可ロジックがルーターに散らばる
変更時の影響範囲が見えなくなります。
ポリシー関数や依存関数へ寄せるのがおすすめです。
13.4 フロントエンド側だけで表示制御して満足する
ボタンを隠しても、APIが叩けるなら意味がありません。
最終的な権限制御はバックエンド側で必ず行います。
13.5 権限設計を文書化しない
ロール名や許可内容が口頭だけだと、実装と運用がずれやすくなります。
OpenAPI、社内ドキュメント、管理画面表記などで揃えると安心です。
14. テスト戦略:認可は「壊れやすい」前提で守る
認可は、機能追加のたびに崩れやすい領域です。
そのため、成功テストだけでなく「拒否されるべきケース」をしっかり書くのが大切です。
14.1 最低限ほしいテスト
adminは更新できるviewerは更新できないmemberは自分の所有物だけ更新できる- 他テナントのリソースにはアクセスできない
- 無料プランではエクスポートできない
- 認可失敗時に監査ログが残る
14.2 ポリシー関数のユニットテスト例
# tests/test_project_policy.py
from app.models.auth import CurrentUser
from app.models.membership import TenantMembership
from app.models.project import Project
from app.policies.project_policy import can_edit_project
def test_admin_can_edit_any_project():
user = CurrentUser(user_id=1, email="a@example.com", tenant_ids=[10], active_tenant_id=10)
membership = TenantMembership(user_id=1, tenant_id=10, role="admin")
project = Project(id=1, tenant_id=10, owner_id=2, name="P", visibility="private")
assert can_edit_project(user, membership, project) is True
def test_member_can_edit_own_project_only():
user = CurrentUser(user_id=1, email="a@example.com", tenant_ids=[10], active_tenant_id=10)
membership = TenantMembership(user_id=1, tenant_id=10, role="member")
own_project = Project(id=1, tenant_id=10, owner_id=1, name="Own", visibility="private")
other_project = Project(id=2, tenant_id=10, owner_id=2, name="Other", visibility="private")
assert can_edit_project(user, membership, own_project) is True
assert can_edit_project(user, membership, other_project) is False
このように、認可ロジックの中心は純粋関数にしておくと、非常にテストしやすくなります。
15. OpenAPI・フロントエンドとの整合も意識する
認可はバックエンドだけの話ではありません。
フロントエンドも「何ができるか」を知る必要があります。
よくあるパターンは次の2つです。
GET /meのレスポンスにロールや許可一覧を含める- テナント情報APIに、プランや有効機能フラグを含める
たとえば、
{
"user_id": 1,
"tenant_id": 10,
"role": "admin",
"permissions": [
"project.read",
"project.create",
"project.update",
"member.invite"
]
}
のような形があると、フロントエンドはUI制御しやすくなります。
ただし、ここで見せた情報は「表示制御の参考」であり、最終判定は必ずバックエンドが行うべきです。
16. 導入ロードマップ
個人開発・学習者さん
- まずはRBACで始める
require_adminのような依存関数を作る- 所有者判定が必要になったら、ポリシー関数へ切り出す
- 主要APIに403のテストを追加する
小規模チームのエンジニアさん
- 現在のロール一覧と権限一覧を表にする
- ルーター直書きの認可ロジックを棚卸しする
- 依存関数とポリシー関数へ段階的に移す
- 監査ログに「拒否理由」を残す
- OpenAPIやフロント向け
/meレスポンスを整える
SaaS開発チーム・スタートアップの皆さま
- グローバルロールとテナント内ロールを分けて整理する
tenant_idを全ての主要データアクセスで必須条件にする- ABACで必要な属性(所有者、プラン、状態)を洗い出す
- 認可ポリシーをコード上で一元管理する
- 契約テストや権限テストをCIに組み込み、破壊的変更を防ぐ
参考リンク
- FastAPI Documentation
- FastAPI Security
- FastAPI Dependencies
- OWASP Authorization Cheat Sheet
- NIST ABAC Guide(概念理解に役立ちます)
まとめ
- RBACは始めやすく、FastAPIの認可設計の第一歩としてとても実用的です。
- しかし、マルチテナントや所有者制御、プラン制限などが入ると、ABACの考え方が必要になります。
- FastAPIでは、認証・テナント・リソース解決を依存関数に寄せ、判断そのものはポリシー関数へ集めると、見通しが良く、テストもしやすくなります。
- 認可は「表示制御」ではなく「バックエンドの責任」として設計し、拒否されるべきケースも必ずテストで守ることが大切です。
- 完璧を最初から目指すより、まずはRBACで型を作り、必要に応じてABACを追加していく流れが現実的です。
次の記事としては、この流れと相性が良い「SaaS向けプラン・請求・機能制限設計」や「FastAPIで作る社内管理画面APIの設計パターン」あたりが自然につながります。

