From 347be0137d93e09e647687c0a1fca35ec85b9ded Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 16:31:34 +0200 Subject: [PATCH] refactor 2fa --- .../submissions_download_controller.rb | 28 +++++++----- app/controllers/submit_form_controller.rb | 23 +++------- .../submit_form_decline_controller.rb | 4 +- .../submit_form_download_controller.rb | 3 +- .../submit_form_draw_signature_controller.rb | 3 +- .../submit_form_invite_controller.rb | 3 +- .../submit_form_values_controller.rb | 8 ++-- lib/submitters/authorized_for_form.rb | 45 +++++++++++++++++++ 8 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 lib/submitters/authorized_for_form.rb diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 4bcd3237..8e76039b 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController Submissions::EnsureResultGenerated.call(last_submitter) - if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter) - Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) + if !signature_valid && !current_user_submitter?(last_submitter) + return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) - return head :not_found + if last_submitter.completed_at < TTL.ago + Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) + + return head :not_found + end end if params[:combined] == 'true' - url = build_combined_url(@submitter) - - if url - render json: [url] - else - head :not_found - end + respond_with_combined(last_submitter) else render json: build_urls(last_submitter) end @@ -48,6 +46,16 @@ class SubmissionsDownloadController < ApplicationController private + def respond_with_combined(submitter) + url = build_combined_url(submitter) + + if url + render json: [url] + else + head :not_found + end + end + def current_user_submitter?(submitter) current_user && current_user.account.submitters.exists?(id: submitter.id) end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index f723aa32..a9e2f88c 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -9,7 +9,7 @@ class SubmitFormController < ApplicationController before_action :load_submitter, only: %i[show update completed] before_action :maybe_render_locked_page, only: :show - before_action :maybe_require_link_2fa, only: %i[show update] + before_action :maybe_require_link_2fa, only: %i[show] CONFIG_KEYS = [].freeze @@ -17,7 +17,7 @@ class SubmitFormController < ApplicationController submission = @submitter.submission return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? - return render :email_2fa if require_email_2fa?(@submitter) + return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @@ -48,7 +48,7 @@ class SubmitFormController < ApplicationController end def update - if require_email_2fa?(@submitter) + unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') }, status: :unprocessable_content end @@ -84,7 +84,9 @@ class SubmitFormController < ApplicationController def completed raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at? - redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter) + return if Submitters::AuthorizedForForm.call(@submitter, current_user, request) + + redirect_to submit_form_path(params[:submit_form_slug]) end def success; end @@ -92,10 +94,7 @@ class SubmitFormController < ApplicationController private def maybe_require_link_2fa - return if @submitter.submission.source != 'link' - return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true - return if cookies.encrypted[:email_2fa_slug] == @submitter.slug - return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id + return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request) redirect_to start_form_path(@submitter.submission.template.slug) end @@ -117,12 +116,4 @@ class SubmitFormController < ApplicationController ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) .preload(:blob).index_by(&:uuid) end - - def require_email_2fa?(submitter) - return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true && - submitter.preferences['require_email_2fa'] != true - return false if cookies.encrypted[:email_2fa_slug] == submitter.slug - - true - end end diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index 918903fe..a8f969c3 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -11,7 +11,9 @@ class SubmitFormDeclineController < ApplicationController submitter.completed_at? || submitter.submission.archived_at? || submitter.submission.expired? || - submitter.submission.template&.archived_at? + submitter.submission.template&.archived_at? || + !Submitters::AuthorizedForForm.call(submitter, current_user, + request) ApplicationRecord.transaction do submitter.update!(declined_at: Time.current) diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index d6e0b692..3ebdc5e2 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController @submitter.submission.template&.archived_at? || AccountConfig.exists?(account_id: @submitter.account_id, key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, - value: false) + value: false) || + !Submitters::AuthorizedForForm.call(@submitter, current_user, request) last_completed_submitter = @submitter.submission.submitters .where.not(id: @submitter.id) diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb index 773eb9e7..5ba141c1 100644 --- a/app/controllers/submit_form_draw_signature_controller.rb +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? - if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? + if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? || + !Submitters::AuthorizedForForm.call(@submitter, current_user, request) return redirect_to submit_form_path(@submitter.slug) end diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index ab1f26c3..bcc848ae 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -45,7 +45,8 @@ class SubmitFormInviteController < ApplicationController !submitter.completed_at? && !submitter.submission.archived_at? && !submitter.submission.expired? && - !submitter.submission.template&.archived_at? + !submitter.submission.template&.archived_at? && + Submitters::AuthorizedForForm.call(submitter, current_user, request) end def filter_invite_submitters(submitter, key = 'invite_by_uuid') diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb index e1a6b9ab..affd37ba 100644 --- a/app/controllers/submit_form_values_controller.rb +++ b/app/controllers/submit_form_values_controller.rb @@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController def index submitter = Submitter.find_by!(slug: params[:submit_form_slug]) - return render json: {} if submitter.completed_at? || submitter.declined_at? - return render json: {} if submitter.submission.template&.archived_at? || + return render json: {} if submitter.completed_at? || + submitter.declined_at? || + submitter.submission.template&.archived_at? || submitter.submission.archived_at? || - submitter.submission.expired? + submitter.submission.expired? || + !Submitters::AuthorizedForForm.call(submitter, current_user, request) value = submitter.values[params['field_uuid']] attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? diff --git a/lib/submitters/authorized_for_form.rb b/lib/submitters/authorized_for_form.rb new file mode 100644 index 00000000..27fa411c --- /dev/null +++ b/lib/submitters/authorized_for_form.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Submitters + module AuthorizedForForm + Unauthorized = Class.new(StandardError) + + module_function + + def call(submitter, current_user, request) + pass_email_2fa?(submitter, request) && pass_link_2fa?(submitter, current_user, request) + end + + def pass_email_2fa?(submitter, request) + return false unless submitter + + return true if submitter.submission.template&.preferences&.dig('require_email_2fa') != true && + submitter.preferences['require_email_2fa'] != true + return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug + + return true if request.params[:two_factor_token].present? && + Submitter.signed_id_verifier.verified(request.params[:two_factor_token], + purpose: :email_two_factor) == submitter.slug + + false + end + + def pass_link_2fa?(submitter, current_user, request) + return false unless submitter + + return true if submitter.submission.source != 'link' + return true unless submitter.submission.template&.preferences&.dig('shared_link_2fa') == true + return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug + return true if submitter.email == current_user&.email && current_user&.account_id == submitter.account_id + + if request.params[:two_factor_token].present? + link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':') + + return true if Submitter.signed_id_verifier.verified(request.params[:two_factor_token], + purpose: :email_two_factor) == link_2fa_key + end + + false + end + end +end