【Supabase】二重の防御で安全設計！RLS + アプリ権限の実践ガイド


## はじめに

データベースの権限管理で頭を悩ませていませんか？

Supabaseの強みは、PostgreSQLの**Row Level Security (RLS)** を活用して行単位のアクセス制御ができる点にあります。

今回は**実際のプロジェクトで得た知見**をもとに、Supabaseを使って、アプリケーション専用の権限テーブルを設計し、**RLSと組み合わせる多層防御**を行う方法を紹介します。

「RLSって何？」という方から、「より堅牢なセキュリティを目指したい」というベテランの方まで、**きっと新しい発見**があるはずです。


---

## 1. RLSとアプリ権限テーブルはなぜ両立させるのか

- **RLS (Row Level Security)**  
  DBレベルで「誰がどの行を読み書きできるか」を制御。
  たとえば自分の投稿のみ閲覧・編集できるようにするなど、1行単位の細かいルール設定が可能です。

- **アプリケーション権限テーブル**  
  アプリのロール・ポリシー管理用に独自設計したテーブル群（ER図参照）から、ユーザーが**どの機能・リソース**にアクセスできるかを判断します。  
  - DBのGRANT機能を使うわけではなく、**ユーザーロールやポリシーテーブルを参照しつつ、アプリが「この操作はOK」と判断してAPIを通す**イメージです。

両者を組み合わせるメリットは、万が一アプリケーション側が誤って不適切な操作を許可してしまっても、**最後の砦としてRLSが行レベルでブロックしてくれる**ところにあります。

---

## 2. anonキーとservice_roleキーの使い分け

### anonキー
- **公開用キー**  
  フロントエンドからのリクエストで使用し、RLS適用下でアクセスします。  
- **アプリ権限テーブルを組み合わせた利用例**  
  1. ユーザーがログインすると、アプリ側が権限テーブルから「このユーザーは読込/書込権限があるか？」を判定  
  2. 許可された操作のみSupabaseのAPIに投げる  
  3. SupabaseはRLSで行単位のアクセス制限を実施

### service_roleキー
- **特権キー**  
  RLSをバイパスでき、**すべての行**に対して操作可能。  
- **利用シーン**  
  - CMSや管理画面など、管理者が全データを確認・操作する必要がある場合  
  - バッチ処理やバックエンドでの一括更新・集計など

この2種類のキーを使い分けることで、**一般ユーザーにはRLSで厳密に行制限**をかけつつ、**管理者はRLSを気にせずすべての行にアクセス**できるようになります。

詳細は以下の記事をご確認ください。


---

## 3. アプリ権限テーブル設計例

下図は、**ロールやポリシーを組み合わせてアプリ側で「ユーザーがどんな操作をできるか」を管理する**ためのER図です。

```mermaid
erDiagram
    roles ||--o{ user_roles : has
    roles ||--o{ role_policies : has
    policies ||--o{ role_policies : has
    policies ||--o{ user_policies : has
    users ||--o{ user_roles : has
    users ||--o{ user_policies : has
    facilities ||--o{ roles : has
    facilities ||--o{ policies : has

    facilities {
        bigint id PK
        varchar name
        varchar description
        timestamp created_at
        timestamp updated_at
    }

    users {
        bigint id PK
        varchar username
        varchar email
        timestamp created_at
        timestamp updated_at
    }

    roles {
        bigint id PK
        bigint facility_id FK
        varchar role_name
        timestamp created_at
        timestamp updated_at
    }

    policies {
        bigint id PK
        bigint facility_id FK
        varchar policy_name
        json policy_document
        timestamp created_at
        timestamp updated_at
    }

    user_roles {
        bigint id PK
        bigint user_id FK
        bigint role_id FK
        bigint facility_id FK
        timestamp created_at
        timestamp updated_at
    }

    role_policies {
        bigint id PK
        bigint role_id FK
        bigint policy_id FK
        timestamp created_at
        timestamp updated_at
    }

    user_policies {
        bigint id PK
        bigint user_id FK
        bigint policy_id FK
        timestamp created_at
        timestamp updated_at
    }

```

### 設計のポイント
1. **ユーザーが所属するロール**  
   - `roles` テーブルに施設ごとのロールを登録  
   - `user_roles` でユーザーとロールを紐づけ  
2. **権限の定義方法**  
   - `policies` テーブルに機能ごとのポリシーを管理（たとえば「在庫閲覧」「在庫追加」「在庫削除」など）  
   - `role_policies` でロールとポリシーを紐づけし、「このロールはどのポリシーを実行できるか」定義  
3. **アプリ起動時またはログイン時に判定**  
   - ユーザーがAPIを叩く前に、ロールやポリシーを参照して「この操作は許可されるか」をチェック  
   - アプリ側の論理で「許可された操作だけ」をSupabaseに渡す

---

## 4. 多層防御の流れ

1. **ログイン**  
   - anonキーを使い、RLS下でDBに接続  
   - 同時にアプリ側で `user_roles` / `user_policies` などを読み込み、「どの操作を実行可能か」を把握  
2. **ユーザーの操作**  
   - ユーザーが在庫追加・レコード編集などを要求  
   - アプリがまず「ロール・ポリシーを満たしているか？」を確認  
   - 権限がある操作だけSupabaseのエンドポイントへリクエスト  
3. **RLSの再チェック**  
   - Supabase側で、RLSが「この行はユーザーに返していいか？」を最終判定  
   - 条件に合わない行は自動的にブロックされる

```mermaid
sequenceDiagram
    actor User
    participant App
    participant Supabase
    participant DB

    Note over User,DB: 1. ログインフェーズ
    User->>App: ログイン要求
    App->>Supabase: anonキーで接続
    Supabase->>DB: RLS下でDB接続
    DB-->>App: user_roles/user_policiesを返却
    App->>App: 実行可能な操作を把握

    Note over User,DB: 2. 操作フェーズ
    User->>App: データ操作要求
    App->>App: ロール・ポリシーチェック
    alt 権限あり
        App->>Supabase: 操作リクエスト
    else 権限なし
        App-->>User: 操作拒否
    end

    Note over User,DB: 3. RLS検証フェーズ
    Supabase->>Supabase: RLSによる行制限チェック
    alt 条件適合
        Supabase->>DB: データ操作実行
        DB-->>User: 結果返却
    else 条件不適合
        Supabase-->>App: アクセス拒否
    end

```

こうすることで、**アプリレベルのロール・ポリシー管理**＋**RLSによる行制限**の二重構造が実現します。


---

## まとめ

1. **アプリケーション側での権限チェック（ER図のテーブル設計）**  
   - ユーザーがどんな操作を許可されているかをロール・ポリシーで一元管理  
   - アプリで「操作OK/NG」を判断し、不要なAPIコールを避ける  
2. **Row Level Security (RLS) による行単位制御**  
   - 万が一アプリが誤ってリクエストしても、行レベルでDBがしっかりとアクセスを制限  
3. **anonキー & service_roleキーの活用**  
   - 一般ユーザーはRLS下のanonキーで安全に操作  
   - 管理者やCMSはservice_roleキーでRLSをバイパスし、すべての行を操作  

このように、**アプリケーション側の権限テーブルによるチェック**と**RLS**を併用することで、多層的にシステムを守ることができます。

誤設定やアプリのバグがあっても、DBレベルで最終防御ラインを敷いておけば安心です。

Supabaseの柔軟な機能をフル活用して、**セキュアで拡張性のあるアプリケーション**を構築してみてください。

---
### 会社紹介

**株式会社 Mosaica**  
最先端テクノロジーで社会課題を解決し、持続可能な未来を創造する IT カンパニー。
AI ソリューション、クラウド統合、DX 推進、経営コンサルティングなど包括的なサービスでビジネス変革を支援しています。  

詳しくは [公式サイト](https://mosaica.co.jp/) までお気軽にご相談ください。
公式サイト： https://mosaica.co.jp/