diff --git a/app/assets/stylesheets/blank-slates.css b/app/assets/stylesheets/blank-slates.css index 3c36b30da..aa221c452 100644 --- a/app/assets/stylesheets/blank-slates.css +++ b/app/assets/stylesheets/blank-slates.css @@ -24,12 +24,12 @@ border: 2px dashed var(--color-ink-light); border-radius: 1ch; color: var(--color-ink-dark); - margin-block-start: 5dvh; + margin-block-start: 2dvh; padding: 1ch 2ch; rotate: -3deg; font-weight: 500; - .cards:not(.cards--grid) & { + .cards:has(.card) & { display: none; } } diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 721e69b0d..5b8763b71 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -30,7 +30,7 @@ class BoardsController < ApplicationController end def edit - selected_user_ids = @board.users.pluck :id + selected_user_ids = @board.users.ids @selected_users, @unselected_users = \ @board.account.users.active.alphabetically.includes(:identity).partition { |user| selected_user_ids.include? user.id } end diff --git a/app/controllers/concerns/request_forgery_protection.rb b/app/controllers/concerns/request_forgery_protection.rb index c8481beb3..fdff7ba8f 100644 --- a/app/controllers/concerns/request_forgery_protection.rb +++ b/app/controllers/concerns/request_forgery_protection.rb @@ -2,31 +2,19 @@ module RequestForgeryProtection extend ActiveSupport::Concern included do - after_action :append_sec_fetch_site_to_vary_header + protect_from_forgery using: :header_only, with: :exception end private - def append_sec_fetch_site_to_vary_header - vary_header = response.headers["Vary"].to_s.split(",").map(&:strip).reject(&:blank?) - response.headers["Vary"] = (vary_header + [ "Sec-Fetch-Site" ]).join(",") + def verified_via_header_only? + super || allowed_api_request? || allowed_insecure_context_request? end - def verified_request? - request.get? || request.head? || !protect_against_forgery? || - (valid_request_origin? && safe_fetch_site?) + def allowed_api_request? + sec_fetch_site_value.nil? && request.format.json? end - SAFE_FETCH_SITES = %w[ same-origin same-site ] - - def safe_fetch_site? - SAFE_FETCH_SITES.include?(sec_fetch_site_value) || (sec_fetch_site_value.nil? && api_request?) - end - - def api_request? - request.format.json? - end - - def sec_fetch_site_value - request.headers["Sec-Fetch-Site"].to_s.downcase.presence + def allowed_insecure_context_request? + sec_fetch_site_value.nil? && !request.ssl? && !Rails.configuration.force_ssl end end diff --git a/app/models/board/storage.rb b/app/models/board/storage.rb index 24028e714..126ba96ab 100644 --- a/app/models/board/storage.rb +++ b/app/models/board/storage.rb @@ -21,7 +21,7 @@ module Board::Storage end def card_ids - @card_ids ||= cards.pluck(:id) + @card_ids ||= cards.ids end def card_image_bytes @@ -36,7 +36,7 @@ module Board::Storage def comment_embed_bytes card_ids.each_slice(BATCH_SIZE).sum do |batch| - sum_embed_bytes_for "Comment", Comment.where(card_id: batch).pluck(:id) + sum_embed_bytes_for "Comment", Comment.where(card_id: batch).ids end end @@ -46,8 +46,7 @@ module Board::Storage def sum_embed_bytes_for(record_type, record_ids) rich_text_ids = ActionText::RichText \ - .where(record_type: record_type, record_id: record_ids) - .pluck(:id) + .where(record_type: record_type, record_id: record_ids).ids sum_blob_bytes_in_batches \ ActiveStorage::Attachment.where(record_type: "ActionText::RichText", name: "embeds"), diff --git a/app/models/user/accessor.rb b/app/models/user/accessor.rb index a9cc5e459..e8a24830f 100644 --- a/app/models/user/accessor.rb +++ b/app/models/user/accessor.rb @@ -13,6 +13,6 @@ module User::Accessor private def grant_access_to_boards - Access.insert_all account.boards.all_access.pluck(:id).collect { |board_id| { id: ActiveRecord::Type::Uuid.generate, board_id: board_id, user_id: id, account_id: account.id } } + Access.insert_all account.boards.all_access.ids.collect { |board_id| { id: ActiveRecord::Type::Uuid.generate, board_id: board_id, user_id: id, account_id: account.id } } end end diff --git a/config/queue.yml b/config/queue.yml index 3f5615fa8..722da40b1 100644 --- a/config/queue.yml +++ b/config/queue.yml @@ -3,7 +3,7 @@ default: &default - polling_interval: 1 batch_size: 500 workers: - - queues: [ "default", "solid_queue_recurring", "backend", "webhooks" ] + - queues: [ "default", "solid_queue_recurring", "backend", "webhooks", "*" ] threads: 3 processes: <%= Integer(ENV.fetch("JOB_CONCURRENCY") { Concurrent.physical_processor_count }) %> polling_interval: 0.1 diff --git a/config/recurring.yml b/config/recurring.yml index 6485278b3..bc1d069cc 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -18,9 +18,6 @@ production: &production clear_solid_queue_finished_jobs: command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" schedule: every hour at minute 12 - clear_solid_queue_recurring_executions: - command: "SolidQueue::RecurringExecution.clear_in_batches" - schedule: every hour at minute 52 cleanup_webhook_deliveries: command: "Webhook::Delivery.cleanup" schedule: every 4 hours at minute 51 @@ -43,9 +40,6 @@ beta: clear_solid_queue_finished_jobs: command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" schedule: every hour at minute 12 - clear_solid_queue_recurring_executions: - command: "SolidQueue::RecurringExecution.clear_in_batches" - schedule: every hour at minute 52 staging: *production development: *production diff --git a/db/queue_schema.rb b/db/queue_schema.rb index c4713f8d1..84bc6b8a6 100644 --- a/db/queue_schema.rb +++ b/db/queue_schema.rb @@ -132,6 +132,7 @@ ActiveRecord::Schema[8.2].define(version: 1) do t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true end + # If these FKs are removed, make sure to periodically run `RecurringExecution.clear_in_batches` add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade diff --git a/test/controllers/concerns/request_forgery_protection_test.rb b/test/controllers/concerns/request_forgery_protection_test.rb index 233c1e361..db1a84e63 100644 --- a/test/controllers/concerns/request_forgery_protection_test.rb +++ b/test/controllers/concerns/request_forgery_protection_test.rb @@ -6,83 +6,16 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest @original_allow_forgery_protection = ActionController::Base.allow_forgery_protection ActionController::Base.allow_forgery_protection = true + + @original_force_ssl = Rails.configuration.force_ssl end teardown do ActionController::Base.allow_forgery_protection = @original_allow_forgery_protection + Rails.configuration.force_ssl = @original_force_ssl end - test "fails if Sec-Fetch-Site is cross-site" do - assert_no_difference -> { Board.count } do - post boards_path, - params: { board: { name: "Test Board" } }, - headers: { "Sec-Fetch-Site" => "cross-site" } - end - - assert_response :unprocessable_entity - end - - test "succeeds with same-origin Sec-Fetch-Site" do - assert_difference -> { Board.count }, +1 do - post boards_path, - params: { board: { name: "Test Board" } }, - headers: { "Sec-Fetch-Site" => "same-origin" } - end - - assert_response :redirect - end - - test "succeeds with same-site Sec-Fetch-Site" do - assert_difference -> { Board.count }, +1 do - post boards_path, - params: { board: { name: "Test Board" } }, - headers: { "Sec-Fetch-Site" => "same-site" } - end - - assert_response :redirect - end - - test "fails with none Sec-Fetch-Site" do - assert_no_difference -> { Board.count } do - post boards_path, - params: { board: { name: "Test Board" } }, - headers: { "Sec-Fetch-Site" => "none" } - end - - assert_response :unprocessable_entity - end - - test "fails when Sec-Fetch-Site header is missing" do - assert_no_difference -> { Board.count } do - post boards_path, params: { board: { name: "Test Board" } } - end - - assert_response :unprocessable_entity - end - - test "GET requests succeed regardless of Sec-Fetch-Site header" do - get board_path(boards(:writebook)), headers: { "Sec-Fetch-Site" => "cross-site" } - - assert_response :success - end - - test "appends Sec-Fetch-Site to Vary header on GET requests" do - get board_path(boards(:writebook)) - - assert_response :success - assert_includes response.headers["Vary"], "Sec-Fetch-Site" - end - - test "appends Sec-Fetch-Site to Vary header on POST requests" do - post boards_path, - params: { board: { name: "Test Board" } }, - headers: { "Sec-Fetch-Site" => "same-origin" } - - assert_response :redirect - assert_includes response.headers["Vary"], "Sec-Fetch-Site" - end - - test "JSON request succeeds with missing Sec-Fetch-Site" do + test "JSON request succeeds with missing Sec-Fetch-Site header" do assert_difference -> { Board.count }, +1 do post boards_path, params: { board: { name: "Test Board" } }, @@ -92,22 +25,37 @@ class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest assert_response :created end - test "JSON request fails with cross-site Sec-Fetch-Site" do + test "HTTP request succeeds with missing Sec-Fetch-Site header when force_ssl is disabled" do + Rails.configuration.force_ssl = false + + assert_difference -> { Board.count }, +1 do + post boards_path, + params: { board: { name: "Test Board" } } + end + + assert_response :redirect + end + + test "HTTP request fails with missing Sec-Fetch-Site header when force_ssl is enabled" do + Rails.configuration.force_ssl = true + assert_no_difference -> { Board.count } do post boards_path, - params: { board: { name: "Test Board" } }, - headers: { "Sec-Fetch-Site" => "cross-site" }, - as: :json + params: { board: { name: "Test Board" } } end assert_response :unprocessable_entity end - private - def csrf_token - @csrf_token ||= begin - get new_board_path - response.body[/name="authenticity_token" value="([^"]+)"/, 1] - end + test "HTTPS request fails with missing Sec-Fetch-Site header" do + Rails.configuration.force_ssl = false + + assert_no_difference -> { Board.count } do + post boards_path, + params: { board: { name: "Test Board" } }, + headers: { "X-Forwarded-Proto" => "https" } end + + assert_response :unprocessable_entity + end end