From 5e82563f41223b7c04c71006aca4df4fdb553737 Mon Sep 17 00:00:00 2001 From: rlindgren Date: Wed, 29 Apr 2026 15:28:44 -0400 Subject: [PATCH] feat(synth): enable RLS support on views with security_invoker Set security_invoker = true on generated views so that Row-Level Security policies on backing tables are evaluated as the querying user, not the view owner. Without this, RLS is silently bypassed when queries go through the view. This is safe for all deployments: when RLS is not enabled on the backing table, security_invoker has no effect. Backing tables are standard PostgreSQL tables (not hypertables), so RLS is fully supported on all data tables. --- internal/tigerfs/fs/synth/build.go | 9 ++++++++- internal/tigerfs/fs/synth/build_test.go | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/tigerfs/fs/synth/build.go b/internal/tigerfs/fs/synth/build.go index 9cec22d..58c87ec 100644 --- a/internal/tigerfs/fs/synth/build.go +++ b/internal/tigerfs/fs/synth/build.go @@ -72,8 +72,15 @@ func GeneratePlainTextTableSQL(schema, name string) string { // the table in tableSchema. For build apps, the view lives in the user's // schema while the table lives in the tigerfs schema. For synthesized views // on existing tables, both schemas may be the same. +// +// The view is created with security_invoker = true (PostgreSQL 15+) so that +// Row-Level Security policies on the backing table are evaluated as the +// querying user, not the view owner. Without this, RLS policies are bypassed +// when queries go through the view, since views execute as their owner by +// default. This is safe for all deployments: when RLS is not enabled on the +// backing table, security_invoker has no effect. func GenerateViewSQL(viewSchema, viewName, tableSchema, tableName string) string { - return fmt.Sprintf(`CREATE VIEW %s.%s AS SELECT * FROM %s.%s`, + return fmt.Sprintf(`CREATE VIEW %s.%s WITH (security_invoker = true) AS SELECT * FROM %s.%s`, db.QuoteIdent(viewSchema), db.QuoteIdent(viewName), db.QuoteIdent(tableSchema), db.QuoteIdent(tableName)) } diff --git a/internal/tigerfs/fs/synth/build_test.go b/internal/tigerfs/fs/synth/build_test.go index fb26203..ef25ba9 100644 --- a/internal/tigerfs/fs/synth/build_test.go +++ b/internal/tigerfs/fs/synth/build_test.go @@ -248,8 +248,8 @@ func TestGenerateBuildSQL_Markdown(t *testing.T) { if !strings.Contains(allSQL, `"tigerfs"."posts"`) { t.Errorf("table should be in tigerfs schema, got:\n%s", allSQL) } - if !strings.Contains(allSQL, `"public"."posts" AS SELECT * FROM "tigerfs"."posts"`) { - t.Errorf("view should be in public schema referencing tigerfs, got:\n%s", allSQL) + if !strings.Contains(allSQL, `"public"."posts" WITH (security_invoker = true) AS SELECT * FROM "tigerfs"."posts"`) { + t.Errorf("view should be in public schema referencing tigerfs with security_invoker, got:\n%s", allSQL) } // Should have 10 statements: schema, resolve_path, table, parent_index, view, comment, // modified_at function + trigger, parent_mtime function + trigger