diff --git a/app/tasks/maintenance/concerns/statements_helpers_concern.rb b/app/tasks/maintenance/concerns/statements_helpers_concern.rb new file mode 100644 index 000000000..658c07b1b --- /dev/null +++ b/app/tasks/maintenance/concerns/statements_helpers_concern.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Maintenance + module StatementsHelpersConcern + extend ActiveSupport::Concern + + included do + # Execute block in transaction with a local statement timeout. + # A value of 0 disable the timeout. + # + # Example: + # def collection + # with_statement_timeout("5min") do + # Dossier.all + # end + # end + def with_statement_timeout(timeout) + ApplicationRecord.transaction do + ApplicationRecord.connection.execute("SET LOCAL statement_timeout = '#{timeout}'") + yield + end + end + end + end +end diff --git a/config/application.rb b/config/application.rb index b57986388..b45f7bd2f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,6 +23,7 @@ module TPS Rails.autoloaders.main.ignore(Rails.root.join('lib/cops')) Rails.autoloaders.main.ignore(Rails.root.join('lib/linters')) Rails.autoloaders.main.ignore(Rails.root.join('lib/tasks/task_helper.rb')) + Rails.autoloaders.main.collapse('app/tasks/maintenance/concerns') config.paths.add Rails.root.join('spec/mailers/previews').to_s, eager_load: true config.autoload_paths << "#{Rails.root}/app/jobs/concerns" diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 9185336fb..4cb15c70d 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -113,6 +113,29 @@ ], "note": "" }, + { + "warning_type": "SQL Injection", + "warning_code": 0, + "fingerprint": "7dc4935d5b68365bedb8f6b953f01b396cff4daa533c98ee56a84249ca5a1f90", + "check_name": "SQL", + "message": "Possible SQL injection", + "file": "app/tasks/maintenance/concerns/statements_helpers_concern.rb", + "line": 19, + "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", + "code": "ApplicationRecord.connection.execute(\"SET LOCAL statement_timeout = '#{timeout}'\")", + "render_path": null, + "location": { + "type": "method", + "class": "Maintenance::StatementsHelpersConcern", + "method": "with_statement_timeout" + }, + "user_input": "timeout", + "confidence": "Medium", + "cwe_id": [ + 89 + ], + "note": "" + }, { "warning_type": "Cross-Site Scripting", "warning_code": 2, diff --git a/lib/templates/maintenance_tasks/task.rb.tt b/lib/templates/maintenance_tasks/task.rb.tt index ef2e399ca..2c0b98570 100644 --- a/lib/templates/maintenance_tasks/task.rb.tt +++ b/lib/templates/maintenance_tasks/task.rb.tt @@ -5,6 +5,8 @@ module <%= tasks_module %> class <%= class_name %>Task < MaintenanceTasks::Task # Documentation: cette tâche modifie les données pour… + include StatementsHelpersConcern + def collection # Collection to be iterated over # Must be Active Record Relation or Array diff --git a/spec/tasks/maintenance/concerns/statements_helpers_concern_spec.rb b/spec/tasks/maintenance/concerns/statements_helpers_concern_spec.rb new file mode 100644 index 000000000..970ca4898 --- /dev/null +++ b/spec/tasks/maintenance/concerns/statements_helpers_concern_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Maintenance::StatementsHelpersConcern do + let(:dummy_class) do + Class.new do + include Maintenance::StatementsHelpersConcern + end + end + + let(:instance) { dummy_class.new } + + describe '#with_statement_timeout' do + it 'applies the statement timeout and raises an error for long-running queries' do + expect { + instance.with_statement_timeout('1ms') do + # Cette requête devrait prendre plus de 1ms et donc déclencher un timeout + ActiveRecord::Base.connection.execute("SELECT pg_sleep(1)") + end + }.to raise_error(ActiveRecord::StatementInvalid, /canceling statement due to statement timeout/i) + end + + it 'allows queries to complete within the timeout and returns the result' do + result = instance.with_statement_timeout('1s') do + ActiveRecord::Base.connection.execute("SELECT 42 AS answer").first['answer'] + end + expect(result).to eq 42 + end + end +end