From 3c616a824cf0ac75417da93c548a66600f806b16 Mon Sep 17 00:00:00 2001 From: Oleksandr Turchyn Date: Thu, 24 Oct 2024 10:11:39 +0300 Subject: [PATCH] show error for cross-environment API key usage --- .rubocop.yml | 2 +- app/controllers/api/api_base_controller.rb | 36 +++++++++++++++++-- spec/factories/accounts.rb | 9 +++++ spec/requests/submissions_spec.rb | 41 ++++++++++++++++++--- spec/requests/submitters_spec.rb | 42 +++++++++++++++++++--- spec/requests/templates_spec.rb | 42 +++++++++++++++++++--- 6 files changed, 156 insertions(+), 16 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 165bc73a..ff5b5c4d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,7 +66,7 @@ RSpec/ExampleLength: Max: 40 RSpec/MultipleMemoizedHelpers: - Max: 6 + Max: 9 Metrics/BlockNesting: Max: 4 diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb index 267fd9bd..514b2f3a 100644 --- a/app/controllers/api/api_base_controller.rb +++ b/app/controllers/api/api_base_controller.rb @@ -25,9 +25,9 @@ module Api render json: { error: 'Too many requests' }, status: :too_many_requests end - if Rails.env.production? + unless Rails.env.development? rescue_from CanCan::AccessDenied do |e| - render json: { error: e.message }, status: :forbidden + render json: { error: access_denied_error_message(e) }, status: :forbidden end rescue_from JSON::ParserError do |e| @@ -39,6 +39,38 @@ module Api private + def access_denied_error_message(error) + return 'Not authorized' if request.headers['X-Auth-Token'].blank? + return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base) + return 'Not authorized' unless error.subject.respond_to?(:account_id) + + linked_account_record_exists = + if current_user.account.testing? + current_user.account.linked_account_accounts.where(account_type: 'testing') + .exists?(account_id: error.subject.account_id) + else + current_user.account.testing_accounts.exists?(id: error.subject.account_id) + end + + return 'Not authorized' unless linked_account_record_exists + + object_name = error.subject.model_name.human + id = error.subject.id + + message = + if current_user.account.testing? + "#{object_name} #{id} not found using testing API key; Use production API key to " \ + "access production #{object_name.downcase.pluralize}." + else + "#{object_name} #{id} not found using production API key; Use testing API key to " \ + "access testing #{object_name.downcase.pluralize}." + end + + Rollbar.warning(message) if defined?(Rollbar) + + message + end + def paginate(relation, field: :id) result = relation.order(field => :desc) .limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min) diff --git a/spec/factories/accounts.rb b/spec/factories/accounts.rb index 785a37b4..b0551e06 100644 --- a/spec/factories/accounts.rb +++ b/spec/factories/accounts.rb @@ -5,5 +5,14 @@ FactoryBot.define do name { Faker::Company.name } locale { 'en-US' } timezone { 'UTC' } + + trait :with_testing_account do + after(:create) do |account| + testing_account = account.dup.tap { |a| a.name = "Testing - #{account.name}" } + testing_account.uuid = SecureRandom.uuid + account.testing_accounts << testing_account + account.save! + end + end end end diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb index fe31dff8..c5d02f4b 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -3,11 +3,17 @@ require 'rails_helper' describe 'Submission API', type: :request do - let!(:account) { create(:account) } - let!(:author) { create(:user, account:) } - let!(:folder) { create(:template_folder, account:) } - let!(:templates) { create_list(:template, 2, account:, author:, folder:) } - let!(:multiple_submitters_template) { create(:template, submitter_count: 3, account:, author:, folder:) } + let(:account) { create(:account, :with_testing_account) } + let(:testing_account) { account.testing_accounts.first } + let(:author) { create(:user, account:) } + let(:testing_author) { create(:user, account: testing_account) } + let(:folder) { create(:template_folder, account:) } + let(:testing_folder) { create(:template_folder, account: testing_account) } + let(:templates) { create_list(:template, 2, account:, author:, folder:) } + let(:multiple_submitters_template) { create(:template, submitter_count: 3, account:, author:, folder:) } + let(:testing_templates) do + create_list(:template, 2, account: testing_account, author: testing_author, folder: testing_folder) + end describe 'GET /api/submissions' do it 'returns a list of submissions' do @@ -41,6 +47,31 @@ describe 'Submission API', type: :request do expect(response).to have_http_status(:ok) expect(response.parsed_body).to eq(JSON.parse(show_submission_body(submission).to_json)) end + + it 'returns an authorization error if test account API token is used with a production submission' do + submission = create(:submission, :with_submitters, :with_events, template: templates[0], created_by_user: author) + + get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': testing_author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submission #{submission.id} not found using testing API key; " \ + 'Use production API key to access production submissions.' }.to_json) + ) + end + + it 'returns an authorization error if production account API token is used with a test submission' do + submission = create(:submission, :with_submitters, :with_events, template: testing_templates[0], + created_by_user: testing_author) + + get "/api/submissions/#{submission.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submission #{submission.id} not found using production API key; " \ + 'Use testing API key to access testing submissions.' }.to_json) + ) + end end describe 'POST /api/submissions' do diff --git a/spec/requests/submitters_spec.rb b/spec/requests/submitters_spec.rb index 37ceecce..ffe35cd9 100644 --- a/spec/requests/submitters_spec.rb +++ b/spec/requests/submitters_spec.rb @@ -3,10 +3,16 @@ require 'rails_helper' describe 'Submitter API', type: :request do - let!(:account) { create(:account) } - let!(:author) { create(:user, account:) } - let!(:folder) { create(:template_folder, account:) } - let!(:templates) { create_list(:template, 2, account:, author:, folder:) } + let(:account) { create(:account, :with_testing_account) } + let(:testing_account) { account.testing_accounts.first } + let(:author) { create(:user, account:) } + let(:testing_author) { create(:user, account: testing_account) } + let(:folder) { create(:template_folder, account:) } + let(:testing_folder) { create(:template_folder, account: testing_account) } + let(:templates) { create_list(:template, 2, account:, author:, folder:) } + let(:testing_templates) do + create_list(:template, 2, account: testing_account, author: testing_author, folder: testing_folder) + end describe 'GET /api/submitters' do it 'returns a list of submitters' do @@ -42,6 +48,34 @@ describe 'Submitter API', type: :request do expect(response).to have_http_status(:ok) expect(response.parsed_body).to eq(JSON.parse(submitter_body(submitter).to_json)) end + + it 'returns an authorization error if test account API token is used with a production submitter' do + submitter = create(:submission, :with_submitters, :with_events, + template: templates[0], + created_by_user: author).submitters.first + + get "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': testing_author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submitter #{submitter.id} not found using " \ + 'testing API key; Use production API key to access production submitters.' }.to_json) + ) + end + + it 'returns an authorization error if production account API token is used with a test submitter' do + submitter = create(:submission, :with_submitters, :with_events, + template: testing_templates[0], + created_by_user: testing_author).submitters.first + + get "/api/submitters/#{submitter.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Submitter #{submitter.id} not found using production API key; " \ + 'Use testing API key to access testing submitters.' }.to_json) + ) + end end describe 'PUT /api/submitters' do diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb index 67430ed8..26bdb546 100644 --- a/spec/requests/templates_spec.rb +++ b/spec/requests/templates_spec.rb @@ -3,10 +3,12 @@ require 'rails_helper' describe 'Templates API', type: :request do - let!(:account) { create(:account) } - let!(:author) { create(:user, account:) } - let!(:folder) { create(:template_folder, account:) } - let!(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } } + let(:account) { create(:account, :with_testing_account) } + let(:testing_account) { account.testing_accounts.first } + let(:author) { create(:user, account:) } + let(:testing_author) { create(:user, account: testing_account) } + let(:folder) { create(:template_folder, account:) } + let(:template_preferences) { { 'request_email_subject' => 'Subject text', 'request_email_body' => 'Body Text' } } describe 'GET /api/templates' do it 'returns a list of templates' do @@ -48,6 +50,38 @@ describe 'Templates API', type: :request do expect(response).to have_http_status(:ok) expect(response.parsed_body).to eq(JSON.parse(template_body(template).to_json)) end + + it 'returns an authorization error if test account API token is used with a production template' do + template = create(:template, account:, + author:, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + get "/api/templates/#{template.id}", headers: { 'x-auth-token': testing_author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Template #{template.id} not found using testing API key; " \ + 'Use production API key to access production templates.' }.to_json) + ) + end + + it 'returns an authorization error if production account API token is used with a test template' do + template = create(:template, account: testing_account, + author: testing_author, + folder:, + external_id: SecureRandom.base58(10), + preferences: template_preferences) + + get "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body).to eq( + JSON.parse({ error: "Template #{template.id} not found using production API key; " \ + 'Use testing API key to access testing templates.' }.to_json) + ) + end end describe 'PUT /api/templates' do