chore(rls): 套用 tool registry canary wave1.1
All checks were successful
Code Review / ai-code-review (push) Successful in 10s

This commit is contained in:
Your Name
2026-05-12 21:15:14 +08:00
parent 1617b73a9d
commit b7af597459
6 changed files with 281 additions and 2 deletions

View File

@@ -0,0 +1,14 @@
-- Rollback for AwoooP RLS Canary Wave 1.1.
-- This only removes the wave1.1 policy and disables RLS on the tool registry.
-- It intentionally does not touch data.
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
DROP POLICY IF EXISTS awooop_mcp_tool_registry_tenant ON awooop_mcp_tool_registry;
ALTER TABLE awooop_mcp_tool_registry NO FORCE ROW LEVEL SECURITY;
ALTER TABLE awooop_mcp_tool_registry DISABLE ROW LEVEL SECURITY;
COMMIT;

View File

@@ -0,0 +1,72 @@
-- AwoooP RLS Canary Wave 1.1: low-row MCP tool registry
-- Date: 2026-05-12
--
-- Scope:
-- - awooop_mcp_tool_registry
--
-- Why this table:
-- Latest production exact count: 4 rows, all project_id='ewoooc'.
-- Runtime read path is MCP Gateway Gate 3 and filters by ctx.project_id.
--
-- Why not awooop_projects in this wave:
-- Operator Console list_tenants() currently expects cross-tenant visibility.
-- A normal tenant policy would hide ewoooc when context is awoooi, so
-- awooop_projects remains blocked until an explicit platform-admin DB path
-- exists.
--
-- Safety:
-- - fail-closed policy only; no NULL/empty-string app.project_id bypass.
-- - aborts if target is missing project_id, has NULL project_id, or has
-- more rows than the reviewed canary cap.
-- - run with a migration/operator role, not through the production app role.
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
DO $$
DECLARE
total_rows bigint;
null_project_rows bigint;
BEGIN
IF to_regclass('public.awooop_mcp_tool_registry') IS NULL THEN
RAISE EXCEPTION 'RLS canary target table does not exist: awooop_mcp_tool_registry';
END IF;
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'awooop_mcp_tool_registry'
AND column_name = 'project_id'
) THEN
RAISE EXCEPTION 'RLS canary target missing project_id: awooop_mcp_tool_registry';
END IF;
SELECT COUNT(*), COUNT(*) FILTER (WHERE project_id IS NULL)
INTO total_rows, null_project_rows
FROM awooop_mcp_tool_registry;
IF null_project_rows <> 0 THEN
RAISE EXCEPTION 'RLS canary target has NULL project_id rows: %, nulls=%',
'awooop_mcp_tool_registry', null_project_rows;
END IF;
IF total_rows > 20 THEN
RAISE EXCEPTION 'RLS canary wave1.1 reviewed cap exceeded: %, rows=%',
'awooop_mcp_tool_registry', total_rows;
END IF;
END
$$;
ALTER TABLE awooop_mcp_tool_registry ENABLE ROW LEVEL SECURITY;
ALTER TABLE awooop_mcp_tool_registry FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS mcp_tool_registry_tenant_isolation ON awooop_mcp_tool_registry;
DROP POLICY IF EXISTS awooop_mcp_tool_registry_tenant ON awooop_mcp_tool_registry;
CREATE POLICY awooop_mcp_tool_registry_tenant ON awooop_mcp_tool_registry
FOR ALL TO awooop_app
USING (project_id = current_setting('app.project_id', TRUE))
WITH CHECK (project_id = current_setting('app.project_id', TRUE));
COMMIT;

View File

@@ -273,16 +273,35 @@ async def collect(exact_counts: bool) -> tuple[list[Check], dict[str, Any]]:
quoted = '"' + row["table_name"].replace('"', '""') + '"'
count_row = await rows(
conn,
f"SELECT :table_name AS table_name, COUNT(*) AS total_rows, COUNT(*) FILTER (WHERE project_id IS NULL) AS null_project_id_rows FROM {quoted}",
{"table_name": row["table_name"]},
f"""
SELECT
:table_name AS table_name,
CAST(:rls_filtered AS boolean) AS rls_filtered,
current_setting('app.project_id', TRUE) AS project_context,
COUNT(*) AS total_rows,
COUNT(*) FILTER (WHERE project_id IS NULL) AS null_project_id_rows
FROM {quoted}
""",
{
"table_name": row["table_name"],
"rls_filtered": bool(row["rls_enabled"]),
},
)
exact_rows.extend(count_row)
evidence["exact_counts"] = exact_rows
null_tables = [row["table_name"] for row in exact_rows if int(row["null_project_id_rows"]) > 0]
rls_filtered_tables = [row["table_name"] for row in exact_rows if row.get("rls_filtered")]
if null_tables:
add(checks, "project_id_backfill", "BLOCKED", f"NULL project_id remains: {', '.join(null_tables)}")
else:
add(checks, "project_id_backfill", "PASS", "no NULL project_id rows in counted tables")
if rls_filtered_tables:
add(
checks,
"exact_counts_scope",
"WARN",
"counts for RLS-enabled tables are tenant-visible only; use operator role for global counts",
)
else:
add(checks, "project_id_backfill", "WARN", "exact counts skipped; rerun with --exact-counts before enabling RLS")
@@ -325,9 +344,12 @@ def print_human(checks: list[Check], evidence: dict[str, Any]) -> None:
)
for row in evidence.get("exact_counts", []):
scope = "rls_filtered" if row.get("rls_filtered") else "global_visible"
print(
"count "
f"{row['table_name']} "
f"scope={scope} "
f"project_context={row.get('project_context')} "
f"total_rows={row['total_rows']} "
f"null_project_id_rows={row['null_project_id_rows']}"
)