@@ -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+
74957718int 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