mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Initial security fixes (#461)
* Initial sec * Update PII fields * FIX add tests * FIX safely read plaintext data on rake backfill * Update user.rb * FIX tests * encryption_ready? block * Test conditional to encryption on --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
307
test/encryption_verification_test.rb
Normal file
307
test/encryption_verification_test.rb
Normal file
@@ -0,0 +1,307 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class EncryptionVerificationTest < ActiveSupport::TestCase
|
||||
# Skip all tests in this file if encryption is not configured.
|
||||
# This allows the test suite to pass in environments without encryption keys.
|
||||
setup do
|
||||
skip "Encryption not configured" unless User.encryption_ready?
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# USER MODEL TESTS
|
||||
# ============================================================================
|
||||
|
||||
test "user email is encrypted and can be looked up" do
|
||||
user = User.create!(
|
||||
email: "encryption-test@example.com",
|
||||
password: "password123",
|
||||
family: families(:dylan_family)
|
||||
)
|
||||
|
||||
# Verify we can find by email (deterministic encryption)
|
||||
found = User.find_by(email: "encryption-test@example.com")
|
||||
assert_equal user.id, found.id
|
||||
|
||||
# Verify case-insensitive lookup works
|
||||
found_upper = User.find_by(email: "ENCRYPTION-TEST@EXAMPLE.COM")
|
||||
assert_equal user.id, found_upper.id
|
||||
|
||||
# Clean up
|
||||
user.destroy
|
||||
end
|
||||
|
||||
test "user email uniqueness validation works with encryption" do
|
||||
user1 = User.create!(
|
||||
email: "unique-test@example.com",
|
||||
password: "password123",
|
||||
family: families(:dylan_family)
|
||||
)
|
||||
|
||||
# Should fail uniqueness
|
||||
user2 = User.new(
|
||||
email: "unique-test@example.com",
|
||||
password: "password123",
|
||||
family: families(:dylan_family)
|
||||
)
|
||||
assert_not user2.valid?
|
||||
assert user2.errors[:email].any?
|
||||
|
||||
user1.destroy
|
||||
end
|
||||
|
||||
test "user names are encrypted and retrievable" do
|
||||
user = users(:family_admin)
|
||||
original_first = user.first_name
|
||||
original_last = user.last_name
|
||||
|
||||
# Update names
|
||||
user.update!(first_name: "EncryptedFirst", last_name: "EncryptedLast")
|
||||
user.reload
|
||||
|
||||
assert_equal "EncryptedFirst", user.first_name
|
||||
assert_equal "EncryptedLast", user.last_name
|
||||
|
||||
# Restore
|
||||
user.update!(first_name: original_first, last_name: original_last)
|
||||
end
|
||||
|
||||
test "user MFA otp_secret is encrypted" do
|
||||
user = users(:family_admin)
|
||||
|
||||
# Setup MFA
|
||||
user.setup_mfa!
|
||||
assert user.otp_secret.present?
|
||||
|
||||
# Reload and verify we can still read it
|
||||
user.reload
|
||||
assert user.otp_secret.present?
|
||||
|
||||
# Verify provisioning URI works
|
||||
assert user.provisioning_uri.present?
|
||||
|
||||
# Clean up
|
||||
user.disable_mfa!
|
||||
end
|
||||
|
||||
test "user unconfirmed_email is encrypted" do
|
||||
user = users(:family_admin)
|
||||
original_email = user.email
|
||||
|
||||
# Set unconfirmed email
|
||||
user.update!(unconfirmed_email: "new-email@example.com")
|
||||
user.reload
|
||||
|
||||
assert_equal "new-email@example.com", user.unconfirmed_email
|
||||
|
||||
# Clean up
|
||||
user.update!(unconfirmed_email: nil)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# INVITATION MODEL TESTS
|
||||
# ============================================================================
|
||||
|
||||
test "invitation token is encrypted and lookups work" do
|
||||
invitation = Invitation.create!(
|
||||
email: "invite-test@example.com",
|
||||
role: "member",
|
||||
inviter: users(:family_admin),
|
||||
family: families(:dylan_family)
|
||||
)
|
||||
|
||||
# Token should be present
|
||||
assert invitation.token.present?
|
||||
token_value = invitation.token
|
||||
|
||||
# Should be able to find by token
|
||||
found = Invitation.find_by(token: token_value)
|
||||
assert_equal invitation.id, found.id
|
||||
|
||||
invitation.destroy
|
||||
end
|
||||
|
||||
test "invitation email is encrypted and scoped uniqueness works" do
|
||||
invitation1 = Invitation.create!(
|
||||
email: "scoped-invite@example.com",
|
||||
role: "member",
|
||||
inviter: users(:family_admin),
|
||||
family: families(:dylan_family)
|
||||
)
|
||||
|
||||
# Same email, same family should fail
|
||||
invitation2 = Invitation.new(
|
||||
email: "scoped-invite@example.com",
|
||||
role: "member",
|
||||
inviter: users(:family_admin),
|
||||
family: families(:dylan_family)
|
||||
)
|
||||
assert_not invitation2.valid?
|
||||
|
||||
invitation1.destroy
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# INVITE CODE MODEL TESTS
|
||||
# ============================================================================
|
||||
|
||||
test "invite code token is encrypted and claim works" do
|
||||
token = InviteCode.generate!
|
||||
assert token.present?
|
||||
|
||||
# Should be able to claim
|
||||
result = InviteCode.claim!(token)
|
||||
assert result
|
||||
|
||||
# Should not be able to claim again (destroyed)
|
||||
result2 = InviteCode.claim!(token)
|
||||
assert_nil result2
|
||||
end
|
||||
|
||||
test "invite code case-insensitive lookup works" do
|
||||
invite_code = InviteCode.create!
|
||||
token = invite_code.token
|
||||
|
||||
# Should find with lowercase
|
||||
found = InviteCode.find_by(token: token.downcase)
|
||||
assert_equal invite_code.id, found.id
|
||||
|
||||
invite_code.destroy
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# SESSION MODEL TESTS
|
||||
# ============================================================================
|
||||
|
||||
test "session user_agent is encrypted" do
|
||||
Current.user_agent = "Mozilla/5.0 Test Browser"
|
||||
Current.ip_address = "192.168.1.100"
|
||||
|
||||
begin
|
||||
session = Session.create!(user: users(:family_admin))
|
||||
|
||||
assert_equal "Mozilla/5.0 Test Browser", session.user_agent
|
||||
assert session.ip_address_digest.present?
|
||||
|
||||
# Reload and verify
|
||||
session.reload
|
||||
assert_equal "Mozilla/5.0 Test Browser", session.user_agent
|
||||
|
||||
# Verify IP hash is consistent
|
||||
expected_hash = Digest::SHA256.hexdigest("192.168.1.100")
|
||||
assert_equal expected_hash, session.ip_address_digest
|
||||
|
||||
session.destroy
|
||||
ensure
|
||||
Current.user_agent = nil
|
||||
Current.ip_address = nil
|
||||
end
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# MOBILE DEVICE MODEL TESTS
|
||||
# ============================================================================
|
||||
|
||||
test "mobile device device_id is encrypted and uniqueness works" do
|
||||
device = MobileDevice.create!(
|
||||
user: users(:family_admin),
|
||||
device_id: "test-device-12345",
|
||||
device_name: "Test iPhone",
|
||||
device_type: "ios"
|
||||
)
|
||||
|
||||
# Should be able to find by device_id
|
||||
found = MobileDevice.find_by(device_id: "test-device-12345", user: users(:family_admin))
|
||||
assert_equal device.id, found.id
|
||||
|
||||
# Same device_id for same user should fail
|
||||
device2 = MobileDevice.new(
|
||||
user: users(:family_admin),
|
||||
device_id: "test-device-12345",
|
||||
device_name: "Another iPhone",
|
||||
device_type: "ios"
|
||||
)
|
||||
assert_not device2.valid?
|
||||
|
||||
# Same device_id for different user should work
|
||||
device3 = MobileDevice.new(
|
||||
user: users(:family_member),
|
||||
device_id: "test-device-12345",
|
||||
device_name: "Their iPhone",
|
||||
device_type: "ios"
|
||||
)
|
||||
assert device3.valid?
|
||||
|
||||
device.destroy
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# PROVIDER ITEM TESTS (if fixtures exist)
|
||||
# ============================================================================
|
||||
|
||||
test "lunchflow item credentials and payloads are encrypted" do
|
||||
skip "No lunchflow items in fixtures" unless LunchflowItem.any?
|
||||
|
||||
item = LunchflowItem.first
|
||||
original_payload = item.raw_payload
|
||||
|
||||
# Should be able to read
|
||||
assert item.api_key.present? || item.raw_payload.present?
|
||||
|
||||
# Update payload
|
||||
item.update!(raw_payload: { test: "data" })
|
||||
item.reload
|
||||
|
||||
assert_equal({ "test" => "data" }, item.raw_payload)
|
||||
|
||||
# Restore
|
||||
item.update!(raw_payload: original_payload)
|
||||
end
|
||||
|
||||
test "lunchflow account payloads are encrypted" do
|
||||
skip "No lunchflow accounts in fixtures" unless LunchflowAccount.any?
|
||||
|
||||
account = LunchflowAccount.first
|
||||
original_payload = account.raw_payload
|
||||
|
||||
# Should be able to read encrypted fields without error
|
||||
account.reload
|
||||
assert_nothing_raised { account.raw_payload }
|
||||
assert_nothing_raised { account.raw_transactions_payload }
|
||||
|
||||
# Update and verify
|
||||
account.update!(raw_payload: { account_test: "value" })
|
||||
account.reload
|
||||
|
||||
assert_equal({ "account_test" => "value" }, account.raw_payload)
|
||||
|
||||
# Restore
|
||||
account.update!(raw_payload: original_payload)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# DATABASE VERIFICATION TESTS
|
||||
# ============================================================================
|
||||
|
||||
test "encrypted fields are not stored as plaintext in database" do
|
||||
user = User.create!(
|
||||
email: "plaintext-check@example.com",
|
||||
password: "password123",
|
||||
first_name: "PlaintextFirst",
|
||||
last_name: "PlaintextLast",
|
||||
family: families(:dylan_family)
|
||||
)
|
||||
|
||||
# Query raw database value
|
||||
raw_email = ActiveRecord::Base.connection.select_value(
|
||||
User.where(id: user.id).select(:email).to_sql
|
||||
)
|
||||
|
||||
# Should NOT be plaintext (should be encrypted blob or different)
|
||||
assert_not_equal "plaintext-check@example.com", raw_email,
|
||||
"Email should be encrypted in database, not stored as plaintext"
|
||||
|
||||
user.destroy
|
||||
end
|
||||
end
|
||||
@@ -80,6 +80,8 @@ class SsoProviderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "encrypts client_secret" do
|
||||
skip "Encryption not configured" unless SsoProvider.encryption_ready?
|
||||
|
||||
provider = SsoProvider.create!(
|
||||
strategy: "openid_connect",
|
||||
name: "encrypted_test",
|
||||
|
||||
Reference in New Issue
Block a user