🔑

Supabaseで安全なCRUD操作を実現するためのセキュリティ設定

に公開

概要

Row Level Security(RLS) は、テーブル内の行へのアクセスを制御する機能で、PostgreSQL の標準機能の一つです。
Supabase でクライアント側から安全に CRUD 操作を行うためには、RLS の適切な設定が必要不可欠です。
例えば、下記のようなケースに対して対応が可能になっています。

  1. 認証済みユーザーが自分のデータのみ CRUD 操作できるようにしたい
  2. ユーザーのロールに応じたデータの操作権限を付与したい
  3. 複数の企業が同じデータベースを共有しているとき、各企業が自社のデータにしかアクセスできないようにしたい

今回は 1 の設定を中心に見ていきます。

よくあるエラーとその原因

エラー例

new row violates row-level security policy for table "your_table"

このエラーは、RLS ポリシーによってデータの挿入が拒否されていることを示しています。

1. RLS ポリシーの基本設定

推奨ポリシー構成

CRUD 操作を想定し、各テーブルに以下の 4 つのポリシーを設定します:

-- 1. SELECT: 認証済みユーザーが自分のデータを表示
CREATE POLICY "Users can view their own data" ON your_table
FOR SELECT TO authenticated
USING (auth.uid() = user_id);

-- 2. INSERT: 認証済みユーザーが自分のデータを挿入
CREATE POLICY "Users can insert their own data" ON your_table
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);

-- 3. UPDATE: 認証済みユーザーが自分のデータを更新
CREATE POLICY "Users can update their own data" ON your_table
FOR UPDATE TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

-- 4. DELETE: 認証済みユーザーが自分のデータを削除
CREATE POLICY "Users can delete their own data" ON your_table
FOR DELETE TO authenticated
USING (auth.uid() = user_id);

重要なポイント

  • UPDATE ポリシー: USING(更新前の条件)とWITH CHECK(更新後の条件)の両方を設定することで、user_idの不正変更を防げます
  • 認証済みユーザーのみ: TO authenticatedにより、未認証ユーザーのアクセスを完全に遮断

2. 自動的な user_id 設定

問題点

クライアント側で毎回user_idを手動設定するのは:

  • 煩雑: 開発者が毎回忘れずに設定する必要がある
  • セキュリティリスク: 間違ったuser_idを設定してしまったり、クライアント側にuser_idが露出してしまい悪用される可能性

解決策: データベーストリガーの活用

-- 1. user_idを自動設定する関数を作成
CREATE OR REPLACE FUNCTION set_user_id()
RETURNS TRIGGER AS $$
BEGIN
  NEW.user_id = auth.uid();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 2. INSERT時にトリガーを実行
CREATE TRIGGER set_user_id_trigger
  BEFORE INSERT ON your_table
  FOR EACH ROW
  EXECUTE FUNCTION set_user_id();

メリット

  • 自動化: クライアント側でuser_idを指定する必要がない
  • セキュリティ: 現在の認証ユーザーの ID が確実に設定される
  • 一貫性: 全てのテーブルで同じ仕組みを使用可能

3. 実装例(React/TypeScript + GraphQL)

GraphQL Mutation(トリガー設定後)

// user_idを含める必要がない!
const CREATE_TRIP = gql`
  mutation CreateTrip($input: TripsInsertInput!) {
    insertTripsOne(object: $input) {
      id
      title
      destination
      user_id # 自動的に設定される
    }
  }
`;

// 使用例
const [createTrip] = useMutation(CREATE_TRIP);

await createTrip({
  variables: {
    input: {
      title: "東京旅行",
      destination: "Tokyo",
      // user_idは自動設定されるため不要
    },
  },
});

Apollo Client 設定

const client = new ApolloClient({
  uri: "YOUR_GRAPHQL_ENDPOINT",
  headers: {
    Authorization: `Bearer ${supabaseToken}`,
    apikey: "YOUR_ANON_KEY",
  },
});

4. セキュリティ上の注意点

避けるべき設定

-- ❌ 危険: publicロールでのアクセス許可
CREATE POLICY "Bad policy" ON your_table
FOR INSERT TO public  -- 未認証でもアクセス可能
WITH CHECK (true);

-- ❌ 危険: UPDATEでwith_checkが未設定
CREATE POLICY "Bad update policy" ON your_table
FOR UPDATE TO authenticated
USING (auth.uid() = user_id);
-- WITH CHECKがないため、user_idを他人のものに変更可能

推奨される追加対策

  1. 定期的なポリシー見直し
  2. 監査ログの有効化
  3. アプリケーション層での二重チェック

5. デバッグ方法

現在のポリシー確認

-- テーブルのポリシー一覧
SELECT * FROM pg_policies WHERE tablename = 'your_table';

-- 現在の認証状態確認
SELECT auth.uid();

一時的なデバッグ用ポリシー

-- 問題特定のために一時的に緩い条件を設定
CREATE POLICY "Debug policy" ON your_table
FOR INSERT TO authenticated
WITH CHECK (true);

まとめ

Supabase で安全な CRUD 操作を実現するためには:

  1. 適切な RLS ポリシーの設定 - 各操作に対する詳細な権限制御
  2. 自動 user_id 設定 - トリガーを使用した確実で安全な実装
  3. セキュリティを保った開発者体験 - 煩雑な設定を自動化

この設定により、セキュリティを保ちながら、開発者が使いやすいシステムを構築できます。特に、データベーストリガーの活用により、クライアント側の実装を簡潔にしながら、セキュリティホールを防ぐことができます。

GitHubで編集を提案

Discussion