Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions internal/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2382,17 +2382,15 @@ func policyReferencesOtherNewTable(policy *ir.RLSPolicy, newTables map[string]st
if expr == "" {
continue
}
exprLower := strings.ToLower(expr)
for qualifiedName := range newTables {
// Skip the policy's own table
if qualifiedName == ownQualified {
continue
}
// Extract the unqualified table name for substring matching.
// Policy expressions may use unqualified or qualified references.
parts := strings.SplitN(qualifiedName, ".", 2)
tableName := parts[len(parts)-1]
if strings.Contains(exprLower, tableName) {
if containsIdentifier(expr, tableName) || containsIdentifier(expr, qualifiedName) {
Comment on lines 2386 to +2393

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Literals Still Match

This still treats any whole-word occurrence as a table dependency, even when the word is inside a SQL string literal or comment. For example, if a new table users is added and a policy only has USING (group_name = 'users'), this returns true and unnecessarily defers the policy even though it does not reference that table. That keeps the false-positive class alive for policy dependency detection.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dollar Identifiers Split

PostgreSQL identifiers can contain $, but containsIdentifier only treats letters, digits, and _ as identifier characters. A new table named foo will therefore match an unrelated identifier like foo$bar in a policy expression, because $ is treated as a boundary. That can still produce substring-style false positives for valid PostgreSQL identifiers.

return true
}
Comment on lines +2393 to 2395
}
Expand Down
24 changes: 24 additions & 0 deletions internal/diff/policy_dependency_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,27 @@ func TestPolicyReferencesNewFunction_BuiltInIgnored(t *testing.T) {
t.Fatalf("expected policy referencing only built-in functions to remain inline")
}
}

func TestPolicyReferencesOtherNewTable_IdentifierAware(t *testing.T) {
lookup := map[string]struct{}{
"public.orders": {},
"public.users": {},
"public.user": {},
}

if !policyReferencesOtherNewTable(&ir.RLSPolicy{
Schema: "public",
Table: "orders",
Using: "EXISTS (SELECT 1 FROM users u WHERE u.id = orders.user_id)",
}, lookup) {
t.Fatalf("expected policy referencing another new table to be deferred")
}

if policyReferencesOtherNewTable(&ir.RLSPolicy{
Schema: "public",
Table: "orders",
Using: "orders_archive.user_id = 1",
}, lookup) {
t.Fatalf("did not expect substring-only matches inside longer identifiers")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS orders (
id integer NOT NULL,
user_name text NOT NULL
);

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER);

CREATE TABLE IF NOT EXISTS "user" (
id integer NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE orders (
id int NOT NULL,
user_name text NOT NULL
);

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY orders_current_user_scope ON orders
FOR SELECT
TO PUBLIC
USING (user_name = CURRENT_USER);

CREATE TABLE "user" (
id int NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- Empty schema (no tables)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"version": "1.0.0",
"pgschema_version": "1.11.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085"
},
"groups": [
{
"steps": [
{
"sql": "CREATE TABLE IF NOT EXISTS orders (\n id integer NOT NULL,\n user_name text NOT NULL\n);",
"type": "table",
"operation": "create",
"path": "public.orders"
},
{
"sql": "ALTER TABLE orders ENABLE ROW LEVEL SECURITY;",
"type": "table.rls",
"operation": "alter",
"path": "public.orders"
},
{
"sql": "CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER);",
"type": "table.policy",
"operation": "create",
"path": "public.orders.orders_current_user_scope"
},
{
"sql": "CREATE TABLE IF NOT EXISTS \"user\" (\n id integer NOT NULL\n);",
"type": "table",
"operation": "create",
"path": "public.user"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS orders (
id integer NOT NULL,
user_name text NOT NULL
);

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER);

CREATE TABLE IF NOT EXISTS "user" (
id integer NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Plan: 2 to add.

Summary by type:
tables: 2 to add

Tables:
+ orders
+ orders_current_user_scope (policy)
~ orders (rls)
+ user

DDL to be executed:
--------------------------------------------------

CREATE TABLE IF NOT EXISTS orders (
id integer NOT NULL,
user_name text NOT NULL
);

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER);

CREATE TABLE IF NOT EXISTS "user" (
id integer NOT NULL
);