green snake
Photo by Pixabay on Pexels.com
目次

FastAPIで実装するRBAC/ABAC権限管理入門:ロール・属性・ポリシーで安全に認可を設計する実務ガイド


要約

  • 認証は「その人が誰か」を確かめる仕組みで、認可は「その人が何をしてよいか」を決める仕組みです。FastAPIの実務では、この認可設計がアプリの安全性と保守性を大きく左右します。
  • 小さなアプリではRBAC(Role-Based Access Control、ロールベース認可)から始めるのが現実的です。admineditorviewer のような役割で権限をまとめると、実装と運用が分かりやすくなります。
  • ただし、SaaSやマルチテナント環境では、ロールだけでは表現しきれない条件が増えます。そこでABAC(Attribute-Based Access Control、属性ベース認可)を組み合わせ、「所属テナント」「所有者かどうか」「公開状態」「プラン」などの属性で判断する設計が重要になります。
  • FastAPIでは、DependsSecurity を使い、認証済みユーザー、テナント、対象リソースを依存関数で解決し、その上でポリシー関数に認可判断を集約すると、見通しが良くなります。
  • 本記事では、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 と一致するなら編集可」
  • 「テナントの planpro 以上ならエクスポート可」
  • 「リソースが 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_id
  • global_role
  • email_verified
  • is_active
  • department
  • plan

9.2 テナント属性

  • tenant_id
  • plan(free / pro / enterprise)
  • status(active / suspended)
  • feature_flags

9.3 リソース属性

  • owner_id
  • tenant_id
  • status(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. 導入ロードマップ

個人開発・学習者さん

  1. まずはRBACで始める
  2. require_admin のような依存関数を作る
  3. 所有者判定が必要になったら、ポリシー関数へ切り出す
  4. 主要APIに403のテストを追加する

小規模チームのエンジニアさん

  1. 現在のロール一覧と権限一覧を表にする
  2. ルーター直書きの認可ロジックを棚卸しする
  3. 依存関数とポリシー関数へ段階的に移す
  4. 監査ログに「拒否理由」を残す
  5. OpenAPIやフロント向け /me レスポンスを整える

SaaS開発チーム・スタートアップの皆さま

  1. グローバルロールとテナント内ロールを分けて整理する
  2. tenant_id を全ての主要データアクセスで必須条件にする
  3. ABACで必要な属性(所有者、プラン、状態)を洗い出す
  4. 認可ポリシーをコード上で一元管理する
  5. 契約テストや権限テストをCIに組み込み、破壊的変更を防ぐ

参考リンク


まとめ

  • RBACは始めやすく、FastAPIの認可設計の第一歩としてとても実用的です。
  • しかし、マルチテナントや所有者制御、プラン制限などが入ると、ABACの考え方が必要になります。
  • FastAPIでは、認証・テナント・リソース解決を依存関数に寄せ、判断そのものはポリシー関数へ集めると、見通しが良く、テストもしやすくなります。
  • 認可は「表示制御」ではなく「バックエンドの責任」として設計し、拒否されるべきケースも必ずテストで守ることが大切です。
  • 完璧を最初から目指すより、まずはRBACで型を作り、必要に応じてABACを追加していく流れが現実的です。

次の記事としては、この流れと相性が良い「SaaS向けプラン・請求・機能制限設計」や「FastAPIで作る社内管理画面APIの設計パターン」あたりが自然につながります。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)