mcpField
S
supabase-policy-guardrails

supabase-policy-guardrails

github.com/jeremylongshore安全2.9k40 次安装2 天前更新
代码仓库

关于 supabase-policy-guardrails

supabase-policy-guardrails 为团队提供 Supabase 治理模式,涵盖可复用的 RLS 策略、命名规范、迁移检查、成本监控和安全审计。适用于需要在 CI/CD 中统一数据库访问控制与安全检查的项目。

主要功能

  1. 01可复用的 RLS 策略模板
  2. 02数据库对象命名规范验证
  3. 03CI 迁移安全检查
  4. 04成本阈值与 Slack 告警
  5. 05安全审计脚本

适用场景

  1. 01为新表应用一致的访问策略
  2. 02在 CI 中拦截缺少 RLS 的迁移
  3. 03监控 Supabase 使用成本
  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 策略库(适用于常见访问模式的可复用模板)、命名规范(表、列、函数、策略)、迁移审查流程(通过 CI 检查确保启用 RLS、防止破坏性操作并强制执行命名规范)、成本告警配置(账单阈值和用量监控),以及安全审计脚本(扫描暴露的密钥、缺少 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,或手动应用迁移

示例

完整示例请参阅 CI、成本和安全参考,其中包括应用 RLS 模板、运行安全审计和检查命名规范。

资源

后续步骤

如需了解不同应用类型的架构模式,请参阅 supabase-architecture-variants。

评论

支持 Markdown