mcpField
S
supabase-policy-guardrails

supabase-policy-guardrails

github.com/jeremylongshoreセキュリティ2.9k40 回インストール2 日前に更新
リポジトリ

supabase-policy-guardrails について

supabase-policy-guardrailsは、Supabaseを運用するチーム向けのガバナンスパターンを提供します。再利用可能なRLSポリシー、命名規則、マイグレーション検査、コスト監視、セキュリティ監査を扱います。

主な機能

  1. 01再利用可能なRLSポリシーテンプレート
  2. 02データベース命名規則の検証
  3. 03CIでのマイグレーションセキュリティ検査
  4. 04コスト閾値とSlackアラート
  5. 05セキュリティ監査スクリプト

活用シーン

  1. 01新しいテーブルに一貫したアクセス方針を適用
  2. 02CIでRLSのないマイグレーションを阻止
  3. 03Supabaseのコストを監視
  4. 04過度に許可的なポリシーや露出キーを監査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jeremylongshore/tons-of-skills-marketplace --skill supabase-policy-guardrails

インストール後、Claude Code で/supabase-policy-guardrailsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
ci-cost-security.md
errors.md
eslint-rules.md
examples.md

詳細

Supabase ポリシーのガードレール

概要

大規模な Supabase の組織的ガバナンスを実現します。共有 RLS ポリシーライブラリ(一般的なアクセスパターン向けの再利用可能なテンプレート)、命名規則(テーブル、列、関数、ポリシー)、マイグレーションレビュープロセス(RLS を確認し、破壊的な操作を防ぎ、命名規則を適用する CI チェック)、コストアラートの設定(請求のしきい値と使用状況の監視)、セキュリティ監査スクリプト(公開されたキー、RLS の欠如、過度に許容的なポリシーをスキャン)を含みます。すべてのパターンで、@supabase/supabase-js の実際の createClient と Supabase CLI コマンドを使用します。

前提条件

  • supabase CLI をインストールし、リンクした Supabase プロジェクト
  • @supabase/supabase-js v2 以降をインストール済み
  • CI/CD パイプライン(GitHub Actions を推奨)
  • psql または Supabase SQL Editor によるデータベースアクセス
  • コストアラートと使用状況 API には Pro プランを推奨

手順

ステップ 1 — 共有 RLS ポリシーライブラリと命名規則
RLS ポリシーテンプレート

チームが新しいテーブルに適用する、再利用可能な RLS ポリシーテンプレートを作成します。これにより、開発者ごとに場当たり的なポリシーが作成されるのを防ぎ、一貫したアクセス制御を実現できます。

-- supabase/migrations/00000000000000_rls_policy_library.sql
-- Shared RLS policy library — apply these templates to new tables

-- ============================================================
-- Template 1: Owner-only access (user owns the row)
-- Usage: tables with a user_id column (todos, profiles, settings)
-- ============================================================
CREATE OR REPLACE FUNCTION public.rls_owner_only(table_name text, user_column text DEFAULT 'user_id')
RETURNS void AS $$
BEGIN
  EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);

  EXECUTE format(
    'CREATE POLICY "owner_select" ON public.%I FOR SELECT USING (%I = auth.uid())',
    table_name, user_column
  );
  EXECUTE format(
    'CREATE POLICY "owner_insert" ON public.%I FOR INSERT WITH CHECK (%I = auth.uid())',
    table_name, user_column
  );
  EXECUTE format(
    'CREATE POLICY "owner_update" ON public.%I FOR UPDATE USING (%I = auth.uid())',
    table_name, user_column
  );
  EXECUTE format(
    'CREATE POLICY "owner_delete" ON public.%I FOR DELETE USING (%I = auth.uid())',
    table_name, user_column
  );
END;
$$ LANGUAGE plpgsql;

-- ============================================================
-- Template 2: Organization-scoped access (user is member of org)
-- Usage: tables with org_id referencing org_members
-- ============================================================
CREATE OR REPLACE FUNCTION public.rls_org_scoped(
  table_name text,
  org_column text DEFAULT 'org_id',
  allow_delete boolean DEFAULT false
)
RETURNS void AS $$
BEGIN
  EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);

  EXECUTE format(
    'CREATE POLICY "org_select" ON public.%I FOR SELECT USING (
      %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid())
    )', table_name, org_column
  );
  EXECUTE format(
    'CREATE POLICY "org_insert" ON public.%I FOR INSERT WITH CHECK (
      %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid())
    )', table_name, org_column
  );
  EXECUTE format(
    'CREATE POLICY "org_update" ON public.%I FOR UPDATE USING (
      %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid() AND role IN (''admin'', ''editor''))
    )', table_name, org_column
  );

  IF allow_delete THEN
    EXECUTE format(
      'CREATE POLICY "org_delete" ON public.%I FOR DELETE USING (
        %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid() AND role = ''admin'')
      )', table_name, org_column
    );
  END IF;
END;
$$ LANGUAGE plpgsql;

-- ============================================================
-- Template 3: Public read, authenticated write
-- Usage: blog posts, product listings, public content
-- ============================================================
CREATE OR REPLACE FUNCTION public.rls_public_read_auth_write(
  table_name text,
  owner_column text DEFAULT 'created_by'
)
RETURNS void AS $$
BEGIN
  EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);

  EXECUTE format(
    'CREATE POLICY "public_select" ON public.%I FOR SELECT USING (true)',
    table_name
  );
  EXECUTE format(
    'CREATE POLICY "auth_insert" ON public.%I FOR INSERT WITH CHECK (auth.uid() IS NOT NULL)',
    table_name
  );
  EXECUTE format(
    'CREATE POLICY "owner_update" ON public.%I FOR UPDATE USING (%I = auth.uid())',
    table_name, owner_column
  );
  EXECUTE format(
    'CREATE POLICY "owner_delete" ON public.%I FOR DELETE USING (%I = auth.uid())',
    table_name, owner_column
  );
END;
$$ LANGUAGE plpgsql;

-- Apply templates to tables:
-- SELECT public.rls_owner_only('todos');
-- SELECT public.rls_org_scoped('projects', 'org_id', true);
-- SELECT public.rls_public_read_auth_write('blog_posts', 'author_id');
命名規則
-- supabase/migrations/00000000000001_naming_convention_check.sql
-- Validation function that checks naming conventions at migration time

CREATE OR REPLACE FUNCTION public.validate_naming_conventions()
RETURNS TABLE(issue text, object_name text, suggestion text) AS $$
BEGIN
  -- Tables must be snake_case, plural
  RETURN QUERY
  SELECT
    'Table name should be plural snake_case'::text,
    t.tablename::text,
    regexp_replace(t.tablename, '([A-Z])', '_\1', 'g')::text
  FROM pg_tables t
  WHERE t.schemaname = 'public'
  AND (
    t.tablename ~ '[A-Z]'           -- contains uppercase
    OR t.tablename ~ '-'             -- contains hyphens
    OR t.tablename !~ 's$'           -- not plural (heuristic)
  )
  AND t.tablename NOT LIKE '\_%';   -- skip internal tables

  -- Columns must be snake_case
  RETURN QUERY
  SELECT
    'Column name should be snake_case'::text,
    (c.table_name || '.' || c.column_name)::text,
    regexp_replace(c.column_name, '([A-Z])', '_\1', 'g')::text
  FROM information_schema.columns c
  WHERE c.table_schema = 'public'
  AND (c.column_name ~ '[A-Z]' OR c.column_name ~ '-');

  -- Foreign key columns should end with _id
  RETURN QUERY
  SELECT
    'Foreign key column should end with _id'::text,
    (tc.table_name || '.' || kcu.column_name)::text,
    (kcu.column_name || '_id')::text
  FROM information_schema.table_constraints tc
  JOIN information_schema.key_column_usage kcu
    ON tc.constraint_name = kcu.constraint_name
  WHERE tc.constraint_type = 'FOREIGN KEY'
  AND tc.table_schema = 'public'
  AND kcu.column_name NOT LIKE '%_id';

  -- Boolean columns should start with is_ or has_
  RETURN QUERY
  SELECT
    'Boolean column should start with is_ or has_'::text,
    (c.table_name || '.' || c.column_name)::text,
    ('is_' || c.column_name)::text
  FROM information_schema.columns c
  WHERE c.table_schema = 'public'
  AND c.data_type = 'boolean'
  AND c.column_name NOT LIKE 'is_%'
  AND c.column_name NOT LIKE 'has_%';
END;
$$ LANGUAGE plpgsql;

-- Run: SELECT * FROM public.validate_naming_conventions();
命名規則のリファレンス
オブジェクト規則例
テーブル複数形の snake_caseuser_profiles、order_items
列snake_casecreated_at、full_name
外部キー{referenced_table_singular}_iduser_id、order_id
ブール値is_ または has_ のプレフィックスis_active、has_verified_email
タイムスタンプ_at のサフィックスcreated_at、updated_at、deleted_at
RLS ポリシー{scope}_{operation}owner_select、org_insert
関数verb_nouncreate_user、get_dashboard_metrics
インデックスidx_{table}_{columns}idx_orders_user_id_created_at
マイグレーション{timestamp}_{verb}_{description}20250322000000_create_orders_table.sql
ステップ 2 — CI チェックを使ったマイグレーションレビュープロセス

GitHub Actions のマイグレーションガードレール(RLS の適用、命名チェック、破壊的操作のブロック)、コミット前フック、Slack アラートを使ったコスト監視、セキュリティ監査スクリプト、スケジュール実行される Edge Function の監査については、CI チェック、コストアラート、セキュリティ監査を参照してください。

出力

  • 所有者のみ、組織スコープ、公開読み取り用のテンプレートを含む共有 RLS ポリシーライブラリ
  • テーブル、列、外部キー、ブール値をチェックする命名規則の検証関数
  • RLS、命名規則、破壊的操作の制御を適用する CI パイプライン
  • ハードコードされたシークレットと RLS のないテーブルをブロックするコミット前フック
  • しきい値を設定でき、Slack にアラートを送信するコスト監視スクリプト
  • RLS の欠如、許容的なポリシー、インデックスの欠如を検出するセキュリティ監査スクリプト
  • 継続的なセキュリティ監視を行う、スケジュール実行される Edge Function

エラー処理

問題原因解決策
新しいテーブルの CI RLS チェックが失敗するマイグレーションに ENABLE ROW LEVEL SECURITY がない同じマイグレーション内で CREATE TABLE の後に ALTER TABLE を追加します
命名規則で誤検出が発生するテーブル名が意図的に単数形になっている(例:config)検証関数の除外リストに追加します
コストアラートが発報されないSUPABASE_ACCESS_TOKEN が設定されていないsupabase.com/dashboard/account/tokens でトークンを生成します
セキュリティ監査がタイムアウトするスキャン対象のテーブルが多すぎる特定のスキーマを対象に監査するか、結果をページネーションします
コミット前チェックがテスト内の正当な JWT をブロックするテストフィクスチャに JWT に似た文字列が含まれている除外パターンにテストファイルのパスを追加します
RLS テンプレート関数が見つからないマイグレーションが適用されていないsupabase db reset を実行するか、マイグレーションを手動で適用します

例

RLS テンプレートの適用、セキュリティ監査の実行、命名規則の確認などの詳しい例については、CI、コスト、セキュリティのリファレンスを参照してください。

リソース

次のステップ

さまざまな種類のアプリにおけるアーキテクチャパターンについては、supabase-architecture-variantsを参照してください。

コメント