🔑
Supabaseで安全なCRUD操作を実現するためのセキュリティ設定
概要
Row Level Security(RLS) は、テーブル内の行へのアクセスを制御する機能で、PostgreSQL の標準機能の一つです。
Supabase でクライアント側から安全に CRUD 操作を行うためには、RLS の適切な設定が必要不可欠です。
例えば、下記のようなケースに対して対応が可能になっています。
- 認証済みユーザーが自分のデータのみ CRUD 操作できるようにしたい
- ユーザーのロールに応じたデータの操作権限を付与したい
- 複数の企業が同じデータベースを共有しているとき、各企業が自社のデータにしかアクセスできないようにしたい
今回は 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を他人のものに変更可能
推奨される追加対策
- 定期的なポリシー見直し
- 監査ログの有効化
- アプリケーション層での二重チェック
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 操作を実現するためには:
- 適切な RLS ポリシーの設定 - 各操作に対する詳細な権限制御
- 自動 user_id 設定 - トリガーを使用した確実で安全な実装
- セキュリティを保った開発者体験 - 煩雑な設定を自動化
この設定により、セキュリティを保ちながら、開発者が使いやすいシステムを構築できます。特に、データベーストリガーの活用により、クライアント側の実装を簡潔にしながら、セキュリティホールを防ぐことができます。
Discussion