Skip to content

Commit 9a5b3fc

Browse files
committed
test(sqlite): add a new test with simulated rls denial using BEFORE triggers with RAISE(ABORT)
1 parent 251aff0 commit 9a5b3fc

File tree

1 file changed

+225
-1
lines changed

1 file changed

+225
-1
lines changed

test/unit.c

Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7492,6 +7492,229 @@ bool do_test_row_filter(int nclients, bool print_result, bool cleanup_databases)
74927492
return result;
74937493
}
74947494

7495+
// Test that BEFORE triggers with RAISE(ABORT) simulate RLS denial:
7496+
// per-PK savepoints isolate failures so allowed rows commit and denied rows roll back.
7497+
bool do_test_rls_trigger_denial (int nclients, bool print_result, bool cleanup_databases, bool only_locals) {
7498+
sqlite3 *db[MAX_SIMULATED_CLIENTS] = {NULL};
7499+
bool result = false;
7500+
int rc = SQLITE_OK;
7501+
7502+
memset(db, 0, sizeof(sqlite3 *) * MAX_SIMULATED_CLIENTS);
7503+
if (nclients >= MAX_SIMULATED_CLIENTS) {
7504+
nclients = MAX_SIMULATED_CLIENTS;
7505+
} else if (nclients < 2) {
7506+
nclients = 2;
7507+
}
7508+
7509+
time_t timestamp = time(NULL);
7510+
int saved_counter = test_counter;
7511+
for (int i = 0; i < nclients; ++i) {
7512+
db[i] = do_create_database_file(i, timestamp, test_counter++);
7513+
if (db[i] == false) return false;
7514+
7515+
rc = sqlite3_exec(db[i], "CREATE TABLE tasks (id TEXT PRIMARY KEY NOT NULL, user_id TEXT, title TEXT, priority INTEGER);", NULL, NULL, NULL);
7516+
if (rc != SQLITE_OK) goto finalize;
7517+
7518+
rc = sqlite3_exec(db[i], "SELECT cloudsync_init('tasks');", NULL, NULL, NULL);
7519+
if (rc != SQLITE_OK) goto finalize;
7520+
}
7521+
7522+
// --- Phase 1: baseline sync (no triggers) ---
7523+
rc = sqlite3_exec(db[0], "INSERT INTO tasks VALUES ('t1', 'user1', 'Task 1', 3);", NULL, NULL, NULL);
7524+
if (rc != SQLITE_OK) goto finalize;
7525+
rc = sqlite3_exec(db[0], "INSERT INTO tasks VALUES ('t2', 'user2', 'Task 2', 5);", NULL, NULL, NULL);
7526+
if (rc != SQLITE_OK) goto finalize;
7527+
rc = sqlite3_exec(db[0], "INSERT INTO tasks VALUES ('t3', 'user1', 'Task 3', 1);", NULL, NULL, NULL);
7528+
if (rc != SQLITE_OK) goto finalize;
7529+
7530+
if (do_merge_using_payload(db[0], db[1], only_locals, true) == false) goto finalize;
7531+
7532+
// Verify: B has 3 rows
7533+
{
7534+
sqlite3_stmt *stmt = NULL;
7535+
rc = sqlite3_prepare_v2(db[1], "SELECT COUNT(*) FROM tasks;", -1, &stmt, NULL);
7536+
if (rc != SQLITE_OK) goto finalize;
7537+
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); goto finalize; }
7538+
int count = sqlite3_column_int(stmt, 0);
7539+
sqlite3_finalize(stmt);
7540+
if (count != 3) {
7541+
printf("Phase 1: expected 3 rows, got %d\n", count);
7542+
goto finalize;
7543+
}
7544+
}
7545+
7546+
// --- Phase 2: INSERT denial with triggers on B ---
7547+
rc = sqlite3_exec(db[1],
7548+
"CREATE TRIGGER rls_deny_insert BEFORE INSERT ON tasks "
7549+
"FOR EACH ROW WHEN NEW.user_id != 'user1' "
7550+
"BEGIN SELECT RAISE(ABORT, 'row violates RLS policy'); END;",
7551+
NULL, NULL, NULL);
7552+
if (rc != SQLITE_OK) goto finalize;
7553+
7554+
rc = sqlite3_exec(db[1],
7555+
"CREATE TRIGGER rls_deny_update BEFORE UPDATE ON tasks "
7556+
"FOR EACH ROW WHEN NEW.user_id != 'user1' "
7557+
"BEGIN SELECT RAISE(ABORT, 'row violates RLS policy'); END;",
7558+
NULL, NULL, NULL);
7559+
if (rc != SQLITE_OK) goto finalize;
7560+
7561+
rc = sqlite3_exec(db[0], "INSERT INTO tasks VALUES ('t4', 'user1', 'Task 4', 2);", NULL, NULL, NULL);
7562+
if (rc != SQLITE_OK) goto finalize;
7563+
rc = sqlite3_exec(db[0], "INSERT INTO tasks VALUES ('t5', 'user2', 'Task 5', 7);", NULL, NULL, NULL);
7564+
if (rc != SQLITE_OK) goto finalize;
7565+
7566+
// Merge with partial-failure tolerance: cloudsync_payload_decode returns error
7567+
// when any PK is denied, but allowed PKs are already committed via per-PK savepoints.
7568+
{
7569+
sqlite3_stmt *sel = NULL, *ins = NULL;
7570+
const char *sel_sql = only_locals
7571+
? "SELECT cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq) FROM cloudsync_changes WHERE site_id=cloudsync_siteid();"
7572+
: "SELECT cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq) FROM cloudsync_changes;";
7573+
rc = sqlite3_prepare_v2(db[0], sel_sql, -1, &sel, NULL);
7574+
if (rc != SQLITE_OK) { sqlite3_finalize(sel); goto finalize; }
7575+
rc = sqlite3_prepare_v2(db[1], "SELECT cloudsync_payload_decode(?);", -1, &ins, NULL);
7576+
if (rc != SQLITE_OK) { sqlite3_finalize(sel); sqlite3_finalize(ins); goto finalize; }
7577+
7578+
while (sqlite3_step(sel) == SQLITE_ROW) {
7579+
sqlite3_value *v = sqlite3_column_value(sel, 0);
7580+
if (sqlite3_value_type(v) == SQLITE_NULL) continue;
7581+
sqlite3_bind_value(ins, 1, v);
7582+
sqlite3_step(ins); // partial failure expected — ignore rc
7583+
sqlite3_reset(ins);
7584+
}
7585+
sqlite3_finalize(sel);
7586+
sqlite3_finalize(ins);
7587+
}
7588+
7589+
// Verify: t4 present (user1 → allowed)
7590+
{
7591+
sqlite3_stmt *stmt = NULL;
7592+
rc = sqlite3_prepare_v2(db[1], "SELECT COUNT(*) FROM tasks WHERE id='t4';", -1, &stmt, NULL);
7593+
if (rc != SQLITE_OK) goto finalize;
7594+
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); goto finalize; }
7595+
int count = sqlite3_column_int(stmt, 0);
7596+
sqlite3_finalize(stmt);
7597+
if (count != 1) {
7598+
printf("Phase 2: t4 expected 1 row, got %d\n", count);
7599+
goto finalize;
7600+
}
7601+
}
7602+
7603+
// Verify: t5 absent (user2 → denied)
7604+
{
7605+
sqlite3_stmt *stmt = NULL;
7606+
rc = sqlite3_prepare_v2(db[1], "SELECT COUNT(*) FROM tasks WHERE id='t5';", -1, &stmt, NULL);
7607+
if (rc != SQLITE_OK) goto finalize;
7608+
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); goto finalize; }
7609+
int count = sqlite3_column_int(stmt, 0);
7610+
sqlite3_finalize(stmt);
7611+
if (count != 0) {
7612+
printf("Phase 2: t5 expected 0 rows, got %d\n", count);
7613+
goto finalize;
7614+
}
7615+
}
7616+
7617+
// Verify: total 4 rows on B (t1, t2, t3 from phase 1 + t4)
7618+
{
7619+
sqlite3_stmt *stmt = NULL;
7620+
rc = sqlite3_prepare_v2(db[1], "SELECT COUNT(*) FROM tasks;", -1, &stmt, NULL);
7621+
if (rc != SQLITE_OK) goto finalize;
7622+
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); goto finalize; }
7623+
int count = sqlite3_column_int(stmt, 0);
7624+
sqlite3_finalize(stmt);
7625+
if (count != 4) {
7626+
printf("Phase 2: expected 4 total rows, got %d\n", count);
7627+
goto finalize;
7628+
}
7629+
}
7630+
7631+
// --- Phase 3: UPDATE denial ---
7632+
rc = sqlite3_exec(db[0], "UPDATE tasks SET title='Task 1 Updated', priority=10 WHERE id='t1';", NULL, NULL, NULL);
7633+
if (rc != SQLITE_OK) goto finalize;
7634+
rc = sqlite3_exec(db[0], "UPDATE tasks SET title='Task 2 Hacked', priority=99 WHERE id='t2';", NULL, NULL, NULL);
7635+
if (rc != SQLITE_OK) goto finalize;
7636+
7637+
// Merge with partial-failure tolerance (same pattern as phase 2)
7638+
{
7639+
sqlite3_stmt *sel = NULL, *ins = NULL;
7640+
const char *sel_sql = only_locals
7641+
? "SELECT cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq) FROM cloudsync_changes WHERE site_id=cloudsync_siteid();"
7642+
: "SELECT cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq) FROM cloudsync_changes;";
7643+
rc = sqlite3_prepare_v2(db[0], sel_sql, -1, &sel, NULL);
7644+
if (rc != SQLITE_OK) { sqlite3_finalize(sel); goto finalize; }
7645+
rc = sqlite3_prepare_v2(db[1], "SELECT cloudsync_payload_decode(?);", -1, &ins, NULL);
7646+
if (rc != SQLITE_OK) { sqlite3_finalize(sel); sqlite3_finalize(ins); goto finalize; }
7647+
7648+
while (sqlite3_step(sel) == SQLITE_ROW) {
7649+
sqlite3_value *v = sqlite3_column_value(sel, 0);
7650+
if (sqlite3_value_type(v) == SQLITE_NULL) continue;
7651+
sqlite3_bind_value(ins, 1, v);
7652+
sqlite3_step(ins); // partial failure expected — ignore rc
7653+
sqlite3_reset(ins);
7654+
}
7655+
sqlite3_finalize(sel);
7656+
sqlite3_finalize(ins);
7657+
}
7658+
7659+
// Verify: t1 updated (user1 → allowed)
7660+
{
7661+
sqlite3_stmt *stmt = NULL;
7662+
rc = sqlite3_prepare_v2(db[1], "SELECT title, priority FROM tasks WHERE id='t1';", -1, &stmt, NULL);
7663+
if (rc != SQLITE_OK) goto finalize;
7664+
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); goto finalize; }
7665+
const char *title = (const char *)sqlite3_column_text(stmt, 0);
7666+
int priority = sqlite3_column_int(stmt, 1);
7667+
bool ok = (strcmp(title, "Task 1 Updated") == 0) && (priority == 10);
7668+
sqlite3_finalize(stmt);
7669+
if (!ok) {
7670+
printf("Phase 3: t1 update not applied (title='%s', priority=%d)\n", title, priority);
7671+
goto finalize;
7672+
}
7673+
}
7674+
7675+
// Verify: t2 unchanged (user2 → denied)
7676+
{
7677+
sqlite3_stmt *stmt = NULL;
7678+
rc = sqlite3_prepare_v2(db[1], "SELECT title, priority FROM tasks WHERE id='t2';", -1, &stmt, NULL);
7679+
if (rc != SQLITE_OK) goto finalize;
7680+
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); goto finalize; }
7681+
const char *title = (const char *)sqlite3_column_text(stmt, 0);
7682+
int priority = sqlite3_column_int(stmt, 1);
7683+
bool ok = (strcmp(title, "Task 2") == 0) && (priority == 5);
7684+
sqlite3_finalize(stmt);
7685+
if (!ok) {
7686+
printf("Phase 3: t2 should be unchanged (title='%s', priority=%d)\n", title, priority);
7687+
goto finalize;
7688+
}
7689+
}
7690+
7691+
result = true;
7692+
rc = SQLITE_OK;
7693+
7694+
finalize:
7695+
for (int i = 0; i < nclients; ++i) {
7696+
if (rc != SQLITE_OK && db[i] && (sqlite3_errcode(db[i]) != SQLITE_OK))
7697+
printf("do_test_rls_trigger_denial error: %s\n", sqlite3_errmsg(db[i]));
7698+
if (db[i]) {
7699+
if (sqlite3_get_autocommit(db[i]) == 0) {
7700+
result = false;
7701+
printf("do_test_rls_trigger_denial error: db %d is in transaction\n", i);
7702+
}
7703+
int counter = close_db(db[i]);
7704+
if (counter > 0) {
7705+
result = false;
7706+
printf("do_test_rls_trigger_denial error: db %d has %d unterminated statements\n", i, counter);
7707+
}
7708+
}
7709+
if (cleanup_databases) {
7710+
char buf[256];
7711+
do_build_database_path(buf, i, timestamp, saved_counter++);
7712+
file_delete_internal(buf);
7713+
}
7714+
}
7715+
return result;
7716+
}
7717+
74957718
int test_report(const char *description, bool result){
74967719
printf("%-30s %s\n", description, (result) ? "OK" : "FAILED");
74977720
return result ? 0 : 1;
@@ -7592,8 +7815,9 @@ int main (int argc, const char * argv[]) {
75927815
result += test_report("Merge Rollback Scenarios:", do_test_merge_rollback_scenarios(2, print_result, cleanup_databases));
75937816
result += test_report("Merge Circular:", do_test_merge_circular(3, print_result, cleanup_databases));
75947817
result += test_report("Merge Foreign Keys:", do_test_merge_foreign_keys(2, print_result, cleanup_databases));
7595-
// Expected failure: TRIGGERs are not fully supported by this extension.
7818+
// Expected failure: AFTER TRIGGERs are not fully supported by this extension.
75967819
// result += test_report("Merge Triggers:", do_test_merge_triggers(2, print_result, cleanup_databases));
7820+
result += test_report("Merge RLS Trigger Denial:", do_test_rls_trigger_denial(2, print_result, cleanup_databases, true));
75977821
result += test_report("Merge Index Consistency:", do_test_merge_index_consistency(2, print_result, cleanup_databases));
75987822
result += test_report("Merge JSON Columns:", do_test_merge_json_columns(2, print_result, cleanup_databases));
75997823
result += test_report("Merge Concurrent Attempts:", do_test_merge_concurrent_attempts(3, print_result, cleanup_databases));

0 commit comments

Comments
 (0)