From 078c48f7fb00a5db66865340a0fcc53adeea9f08 Mon Sep 17 00:00:00 2001 From: Rosa Gutierrez Date: Fri, 29 May 2026 14:40:41 +0200 Subject: [PATCH] Fix auditable-tables shield crashing on insert_all/upsert_all The connection shield assumes its first argument is a SQL String and calls `sql.b`, but `exec_insert_all` receives an ActiveRecord::InsertAll, which doesn't respond to `#b`. So any `insert_all`/`upsert_all` run from an audited console raised `NoMethodError: undefined method 'b' for an instance of ActiveRecord::InsertAll`. It only surfaced in the console because the check is guarded by `executing_user_command?`. Coerce the argument before the auditable-tables check: for a non-String, use its target table name (`InsertAll#model.table_name`) so the regexp still detects access to audited tables. The underlying adapter still receives the original argument untouched, and string SQL is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../protected_auditable_tables.rb | 10 +++- .../protected_auditable_tables_test.rb | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 test/ext/active_record/protected_auditable_tables_test.rb diff --git a/lib/console1984/ext/active_record/protected_auditable_tables.rb b/lib/console1984/ext/active_record/protected_auditable_tables.rb index 28747b3..f33dfc0 100644 --- a/lib/console1984/ext/active_record/protected_auditable_tables.rb +++ b/lib/console1984/ext/active_record/protected_auditable_tables.rb @@ -5,7 +5,7 @@ module Console1984::Ext::ActiveRecord::ProtectedAuditableTables %i[ execute exec_query exec_insert exec_delete exec_update exec_insert_all ].each do |method| define_method method do |*args, **kwargs| sql = args.first - if Console1984.command_executor.executing_user_command? && sql.b =~ auditable_tables_regexp + if Console1984.command_executor.executing_user_command? && auditable_sql(sql) =~ auditable_tables_regexp raise Console1984::Errors::ForbiddenCommandAttempted, "#{sql}" else super(*args, **kwargs) @@ -14,6 +14,14 @@ module Console1984::Ext::ActiveRecord::ProtectedAuditableTables end private + # exec_insert_all receives an ActiveRecord::InsertAll, not a SQL string, so + # #b is undefined on it. Check its target table name instead, so insert_all + # and upsert_all don't blow up when run from the console. + def auditable_sql(sql) + string = sql.is_a?(String) ? sql : (sql.try(:model)&.table_name || sql.to_s) + string.b + end + def auditable_tables_regexp @auditable_tables_regexp ||= Regexp.new("#{auditable_tables.join("|")}") end diff --git a/test/ext/active_record/protected_auditable_tables_test.rb b/test/ext/active_record/protected_auditable_tables_test.rb new file mode 100644 index 0000000..dc7e4fe --- /dev/null +++ b/test/ext/active_record/protected_auditable_tables_test.rb @@ -0,0 +1,55 @@ +require "test_helper" + +class Console1984::Ext::ActiveRecord::ProtectedAuditableTablesTest < ActiveSupport::TestCase + # Stands in for the underlying adapter; the shield is prepended on top. + class FakeConnection + attr_reader :received + + def exec_insert_all(*args, **kwargs) + @received = args.first + :executed + end + + prepend Console1984::Ext::ActiveRecord::ProtectedAuditableTables + end + + # Mimics what exec_insert_all actually receives: an ActiveRecord::InsertAll, + # which responds to #model but not to #b. + InsertAll = Struct.new(:model) + + setup do + # Make sure the audit models are loaded so auditable_tables_regexp isn't empty. + [ Console1984::Session, Console1984::Command, Console1984::SensitiveAccess, Console1984::User ] + @connection = FakeConnection.new + end + + test "an InsertAll for a non-auditable table is allowed and passed through untouched" do + insert_all = InsertAll.new(Person) + + result = as_user { @connection.exec_insert_all(insert_all, "Person Bulk Insert") } + + assert_equal :executed, result + assert_same insert_all, @connection.received + end + + test "an InsertAll for an auditable table is still forbidden" do + insert_all = InsertAll.new(Console1984::Session) + + assert_raises Console1984::Errors::ForbiddenCommandAttempted do + as_user { @connection.exec_insert_all(insert_all, "Session Bulk Insert") } + end + end + + test "string SQL is still checked against auditable tables" do + assert_raises Console1984::Errors::ForbiddenCommandAttempted do + as_user { @connection.exec_insert_all("INSERT INTO console1984_sessions (reason) VALUES ('x')", "x") } + end + + assert_equal :executed, as_user { @connection.exec_insert_all("INSERT INTO people (name) VALUES ('x')", "x") } + end + + private + def as_user(&block) + Console1984.command_executor.run_as_user(&block) + end +end