Skip to content

Commit 4245ae5

Browse files
committed
Disconnect clients when updating RLS rules
1 parent 34b4a2b commit 4245ae5

File tree

3 files changed

+132
-0
lines changed

3 files changed

+132
-0
lines changed

crates/schema/src/auto_migrate.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,13 +1019,26 @@ fn auto_migrate_constraints(plan: &mut AutoMigratePlan, new_tables: &HashSet<&Id
10191019
// Because we can refer to many tables and fields on the row level-security query, we need to remove all of them,
10201020
// then add the new ones, instead of trying to track the graph of dependencies.
10211021
fn auto_migrate_row_level_security(plan: &mut AutoMigratePlan) -> Result<()> {
1022+
// Track if any RLS rules were changed.
1023+
let mut old_rls = HashSet::new();
1024+
let mut new_rls = HashSet::new();
1025+
10221026
for rls in plan.old.row_level_security() {
1027+
old_rls.insert(rls.key());
10231028
plan.steps.push(AutoMigrateStep::RemoveRowLevelSecurity(rls.key()));
10241029
}
10251030
for rls in plan.new.row_level_security() {
1031+
new_rls.insert(rls.key());
10261032
plan.steps.push(AutoMigrateStep::AddRowLevelSecurity(rls.key()));
10271033
}
10281034

1035+
// After changing an RLS rule and updating the module, the updated rule does not take effect for clients until they reconnect.
1036+
// This is because subscriptions are cached and we don't flush the cache when RLS rules are updated.
1037+
// We can force flush the cache by force disconnecting all clients if an RLS rule has been added, removed, or updated.
1038+
if old_rls != new_rls && !plan.disconnects_all_users() {
1039+
plan.steps.push(AutoMigrateStep::DisconnectAllUsers);
1040+
}
1041+
10291042
Ok(())
10301043
}
10311044

@@ -2245,4 +2258,52 @@ mod tests {
22452258
);
22462259
}
22472260
}
2261+
2262+
#[test]
2263+
fn change_rls_disconnect_clients() {
2264+
let old_def = create_module_def(|_builder| {});
2265+
2266+
let new_def = create_module_def(|_builder| {});
2267+
2268+
let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2269+
assert!(!plan.disconnects_all_users(), "{plan:#?}");
2270+
2271+
let old_def = create_module_def(|builder| {
2272+
builder.add_row_level_security("SELECT true;");
2273+
});
2274+
let new_def = create_module_def(|builder| {
2275+
builder.add_row_level_security("SELECT false;");
2276+
});
2277+
2278+
let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2279+
assert!(plan.disconnects_all_users(), "{plan:#?}");
2280+
2281+
let old_def = create_module_def(|builder| {
2282+
builder.add_row_level_security("SELECT true;");
2283+
});
2284+
2285+
let new_def = create_module_def(|_builder| {
2286+
// Remove RLS
2287+
});
2288+
let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2289+
assert!(plan.disconnects_all_users(), "{plan:#?}");
2290+
2291+
let old_def = create_module_def(|_builder| {});
2292+
2293+
let new_def = create_module_def(|builder| {
2294+
builder.add_row_level_security("SELECT false;");
2295+
});
2296+
let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2297+
assert!(plan.disconnects_all_users(), "{plan:#?}");
2298+
2299+
let old_def = create_module_def(|builder| {
2300+
builder.add_row_level_security("SELECT true;");
2301+
});
2302+
2303+
let new_def = create_module_def(|builder| {
2304+
builder.add_row_level_security("SELECT true;");
2305+
});
2306+
let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2307+
assert!(!plan.disconnects_all_users(), "{plan:#?}");
2308+
}
22482309
}

crates/schema/src/snapshots/spacetimedb_schema__auto_migrate__tests__empty_to_populated_migration.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ expression: "plan.pretty_print(PrettyPrintStyle::AnsiColor).expect(\"should pret
6161

6262
▸ Created row level security policy:
6363
`SELECT * FROM Apples`
64+
!!! Warning: All clients will be disconnected due to breaking schema changes

smoketests/tests/rls.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
from .. import Smoketest, random_string
24

35
class Rls(Smoketest):
@@ -78,3 +80,71 @@ def test_publish_fails_for_rls_on_private_table(self):
7880

7981
with self.assertRaises(Exception):
8082
self.publish_module(name)
83+
84+
class DisconnectRls(Smoketest):
85+
AUTOPUBLISH = False
86+
87+
MODULE_CODE = """
88+
use spacetimedb::{Identity, ReducerContext, Table};
89+
90+
#[spacetimedb::table(name = users, public)]
91+
pub struct Users {
92+
name: String,
93+
identity: Identity,
94+
}
95+
96+
#[spacetimedb::reducer]
97+
pub fn add_user(ctx: &ReducerContext, name: String) {
98+
ctx.db.users().insert(Users { name, identity: ctx.sender });
99+
}
100+
101+
// RLS
102+
"""
103+
104+
ADD_RLS = """
105+
#[spacetimedb::client_visibility_filter]
106+
const USER_FILTER: spacetimedb::Filter = spacetimedb::Filter::Sql(
107+
"SELECT * FROM users WHERE identity = :sender"
108+
);
109+
"""
110+
111+
def test_rls_disconnect_if_change(self):
112+
"""This tests that changing the RLS rules disconnects existing clients"""
113+
114+
name = random_string()
115+
116+
self.write_module_code(self.MODULE_CODE)
117+
118+
self.publish_module(name)
119+
logging.info("Initial publish complete")
120+
121+
# Now add the RLS rules
122+
self.write_module_code(self.MODULE_CODE + self.ADD_RLS)
123+
self.publish_module(name, clear=False, break_clients=True)
124+
125+
logging.info("Re-publish with RLS complete")
126+
127+
logs = self.logs(100)
128+
129+
# Validate disconnect + schema migration logs
130+
self.assertIn("Disconnecting all users", logs)
131+
132+
def test_rls_disconnect_no(self):
133+
"""This tests that not changing the RLS rules does not disconnect existing clients"""
134+
135+
name = random_string()
136+
137+
self.write_module_code(self.MODULE_CODE + self.ADD_RLS)
138+
139+
self.publish_module(name)
140+
logging.info("Initial publish complete")
141+
142+
# Now re-publish the same module code
143+
self.publish_module(name, clear=False, break_clients=False)
144+
145+
logging.info("Re-publish without RLS change complete")
146+
147+
logs = self.logs(100)
148+
149+
# Validate no disconnect logs
150+
self.assertNotIn("Disconnecting all users", logs)

0 commit comments

Comments
 (0)