mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 00:24:15 +00:00
* fix(wise): refuse an SCA private key when encryption is unavailable WiseItem wraps its `encrypts` declarations in `if encryption_ready?`, which is false on any install that has not explicitly configured Active Record encryption. On those installs the declaration never runs, so assigning sca_private_key writes the PEM into the column verbatim. That is a tolerable degraded mode for a display name. It is not one for the key that signs Wise balance-statement requests, and nothing in the flow told the user it had happened: the panel reported a keypair as generated either way. generate_sca_keypair! now raises SCAEncryptionUnavailable instead of writing, and a validation refuses the attribute on every other write path. The exception is raised rather than returned so no caller can read "not stored" as "stored". WiseItemsController#generate_sca_keypair already rescues broadly, so the user sees the same panel error as any other keypair failure rather than a 500. The three existing tests that generate a keypair now stub encryption_ready? to true. The test environment configures no encryption keys, so without the stub they would be exercising the refused path rather than the one they describe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wise): validate the SCA key only when it is being written Review found a real regression in the first commit, and CI found a scanner hit. The validation ran on every save. An install that generated a key before this change still has that plaintext value in the column, so the record became permanently unsaveable: renaming the connection failed, and the destroy path failed worse. WiseItemsController#destroy calls unlink_all! and only then destroy_later, whose update!(scheduled_for_deletion: true) would now raise, so the accounts were already unlinked while the provider stayed active. Refusing a NEW key is the point; refusing to let go of an old one is not. The validation now returns unless sca_private_key is actually changing, and the explicit guard in generate_sca_keypair! is unchanged. The regression test fails without the guard, on the reload-and-save assertion. pipelock flagged the literal "BEGIN RSA PRIVATE KEY" header in the test as a critical Private Key Header finding in the diff, which is exactly what a secret scanner should do. The value only ever needed to be non-blank, and the file already uses a plain placeholder two tests above, so it now uses one too. Also adds the encrypted-attributes assertion the other Encryptable models carry, in their shape: it skips when encryption is unconfigured, because the suite deliberately runs that way (see EncryptionVerificationTest's own comment) and turning ENV-based encryption on globally would change encryption_ready? for every Encryptable model, well outside this change. 49 Wise tests green, 1 skipped by that convention. Rubocop clean, Brakeman 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
165 lines
6.6 KiB
Ruby
165 lines
6.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
class WiseItemsControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
sign_in users(:family_admin)
|
|
SyncJob.stubs(:perform_later)
|
|
@family = families(:dylan_family)
|
|
@wise_item = wise_items(:one)
|
|
|
|
@valid_profiles = [
|
|
{ "id" => "99999999", "type" => "personal", "details" => { "firstName" => "Jane", "lastName" => "Doe" } }
|
|
]
|
|
end
|
|
|
|
# create redirects to select_profiles (Turbo requires a redirect from a standard
|
|
# form submission) — the encrypted token travels via the session, not the response body.
|
|
|
|
test "create redirects to select_profiles and keeps raw token out of the session" do
|
|
Provider::Wise.any_instance.stubs(:get_profiles).returns(@valid_profiles)
|
|
|
|
post wise_items_url, params: { wise_item: { token: "live_token_abc" } }
|
|
|
|
assert_redirected_to select_profiles_wise_items_path
|
|
assert_nil session[:wise_pending_token], "raw API token must not be stored in the session"
|
|
assert session[:wise_pending_encrypted_token].present?
|
|
|
|
follow_redirect!
|
|
assert_select "input[name='encrypted_pending_token']"
|
|
end
|
|
|
|
test "create stores an encrypted token that round-trips to the original value" do
|
|
Provider::Wise.any_instance.stubs(:get_profiles).returns(@valid_profiles)
|
|
|
|
post wise_items_url, params: { wise_item: { token: "live_token_abc" } }
|
|
follow_redirect!
|
|
|
|
encrypted = css_select("input[name='encrypted_pending_token']").first["value"]
|
|
assert encrypted.present?, "hidden encrypted_pending_token field must be present"
|
|
|
|
key = Rails.application.key_generator.generate_key("wise_pending_token", 32)
|
|
decrypted = ActiveSupport::MessageEncryptor.new(key).decrypt_and_verify(encrypted)
|
|
assert_equal "live_token_abc", decrypted
|
|
end
|
|
|
|
test "create redirects to providers on blank token" do
|
|
post wise_items_url, params: { wise_item: { token: "" } }
|
|
assert_redirected_to settings_providers_path
|
|
assert_nil session[:wise_pending_token]
|
|
end
|
|
|
|
test "create redirects to providers when Wise API rejects the token" do
|
|
Provider::Wise.any_instance.stubs(:get_profiles).raises(
|
|
Provider::Wise::WiseError.new("unauthorized", :unauthorized)
|
|
)
|
|
|
|
post wise_items_url, params: { wise_item: { token: "bad_token" } }
|
|
assert_redirected_to settings_providers_path
|
|
assert_nil session[:wise_pending_token]
|
|
end
|
|
|
|
# link_profiles reads the encrypted token from the session (set by create) —
|
|
# the client no longer needs to (and cannot) supply or tamper with it via params.
|
|
|
|
test "link_profiles creates WiseItems using the session-held encrypted token" do
|
|
Provider::Wise.any_instance.stubs(:get_profiles).returns(@valid_profiles)
|
|
post wise_items_url, params: { wise_item: { token: "live_token_abc" } }
|
|
|
|
assert_difference "WiseItem.count", 1 do
|
|
post link_profiles_wise_items_url, params: { profile_ids: [ "99999999" ] }
|
|
end
|
|
|
|
assert_redirected_to settings_providers_path
|
|
assert_equal "live_token_abc", @family.wise_items.find_by!(profile_id: "99999999").token
|
|
assert_nil session[:wise_pending_profiles]
|
|
assert_nil session[:wise_pending_encrypted_token]
|
|
end
|
|
|
|
test "link_profiles applies the pending import_all_history setting to created items" do
|
|
Provider::Wise.any_instance.stubs(:get_profiles).returns(@valid_profiles)
|
|
post wise_items_url, params: { wise_item: { token: "live_token_abc", import_all_history: "1" } }
|
|
|
|
assert_difference "WiseItem.count", 1 do
|
|
post link_profiles_wise_items_url, params: { profile_ids: [ "99999999" ] }
|
|
end
|
|
|
|
assert @family.wise_items.find_by!(profile_id: "99999999").import_all_history?
|
|
assert_nil session[:wise_pending_import_all_history]
|
|
end
|
|
|
|
test "link_profiles defaults import_all_history to false when not requested" do
|
|
Provider::Wise.any_instance.stubs(:get_profiles).returns(@valid_profiles)
|
|
post wise_items_url, params: { wise_item: { token: "live_token_abc" } }
|
|
|
|
post link_profiles_wise_items_url, params: { profile_ids: [ "99999999" ] }
|
|
|
|
assert_not @family.wise_items.find_by!(profile_id: "99999999").import_all_history?
|
|
end
|
|
|
|
test "link_profiles applies import_all_history to every created profile" do
|
|
profiles = [
|
|
{ "id" => "99999999", "type" => "personal", "details" => { "firstName" => "Jane", "lastName" => "Doe" } },
|
|
{ "id" => "88888888", "type" => "business", "details" => { "name" => "Acme" } }
|
|
]
|
|
Provider::Wise.any_instance.stubs(:get_profiles).returns(profiles)
|
|
post wise_items_url, params: { wise_item: { token: "live_token_abc", import_all_history: "1" } }
|
|
|
|
assert_difference "WiseItem.count", 2 do
|
|
post link_profiles_wise_items_url, params: { profile_ids: [ "99999999", "88888888" ] }
|
|
end
|
|
|
|
assert @family.wise_items.find_by!(profile_id: "99999999").import_all_history?
|
|
assert @family.wise_items.find_by!(profile_id: "88888888").import_all_history?
|
|
assert_nil session[:wise_pending_import_all_history]
|
|
end
|
|
|
|
test "link_profiles redirects to providers when there is no pending session" do
|
|
post link_profiles_wise_items_url, params: { profile_ids: [ "99999999" ] }
|
|
|
|
assert_redirected_to settings_providers_path
|
|
end
|
|
|
|
# The key is only ever stored encrypted, and the test environment configures
|
|
# no encryption keys, so the flow has to be exercised as an install that does.
|
|
test "generate_sca_keypair stores a keypair on the item" do
|
|
WiseItem.stubs(:encryption_ready?).returns(true)
|
|
assert_nil @wise_item.sca_private_key
|
|
|
|
post generate_sca_keypair_wise_item_url(@wise_item)
|
|
|
|
assert_redirected_to accounts_path
|
|
assert @wise_item.reload.sca_configured?
|
|
end
|
|
|
|
test "generate_sca_keypair replaces a previously generated keypair" do
|
|
WiseItem.stubs(:encryption_ready?).returns(true)
|
|
@wise_item.generate_sca_keypair!
|
|
previous_key = @wise_item.sca_private_key
|
|
|
|
post generate_sca_keypair_wise_item_url(@wise_item)
|
|
|
|
assert_not_equal previous_key, @wise_item.reload.sca_private_key
|
|
end
|
|
|
|
test "generate_sca_keypair reports an error rather than storing a key in the clear" do
|
|
WiseItem.stubs(:encryption_ready?).returns(false)
|
|
|
|
post generate_sca_keypair_wise_item_url(@wise_item)
|
|
|
|
assert_nil @wise_item.reload.sca_private_key
|
|
end
|
|
|
|
test "link_profiles redirects to providers when the session token cannot be decrypted" do
|
|
Provider::Wise.any_instance.stubs(:get_profiles).returns(@valid_profiles)
|
|
post wise_items_url, params: { wise_item: { token: "live_token_abc" } }
|
|
|
|
session[:wise_pending_encrypted_token] = "corrupted_garbage_value"
|
|
|
|
post link_profiles_wise_items_url, params: { profile_ids: [ "99999999" ] }
|
|
|
|
assert_redirected_to settings_providers_path
|
|
end
|
|
end
|