Skip to content

Commit

Permalink
Add encrypted_string as a possible type for Preferences
Browse files Browse the repository at this point in the history
The new type encrypted_string encrypt the content of the preferences,
the original value can be read-only using the preferences accessor method.

It's still recommended to use env variables to manage secrets,
but if it's needed to save the preference on the database, now it's
possible to use encrypted_string to add a security layer.

The encryption key can be passed using the encryption_key option of the preference,
if the option is missing the system fallback to the env variable SOLIDUS_PREFERENCES_MASTER_KEY,
it even this env variable is missing the system will use the Rails master key.

If a default value it's assigned to an encrypted_string preference, this will not be encrypted.
  • Loading branch information
Stefano Sarioli authored and stefano-sarioli committed Aug 10, 2020
1 parent 97f6fe3 commit e05da68
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 4 deletions.
4 changes: 3 additions & 1 deletion core/lib/spree/preferences/preferable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,13 @@ def admin_form_preference_names

private

def convert_preference_value(value, type)
def convert_preference_value(value, type, preference_encryptor = nil)
return nil if value.nil?
case type
when :string, :text
value.to_s
when :encrypted_string
preference_encryptor.encrypt(value.to_s)
when :password
value.to_s
when :decimal
Expand Down
24 changes: 21 additions & 3 deletions core/lib/spree/preferences/preferable_class_methods.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'spree/encryptor'

module Spree::Preferences
module PreferableClassMethods
DEFAULT_ADMIN_FORM_PREFERENCE_TYPES = %i(
Expand All @@ -16,7 +18,13 @@ def defined_preferences
end

def preference(name, type, options = {})
options.assert_valid_keys(:default)
options.assert_valid_keys(:default, :encryption_key)

if type == :encrypted_string
preference_encryptor = preference_encryptor(options)
options[:default] = preference_encryptor.encrypt(options[:default])
end

default = options[:default]
default = ->{ options[:default] } unless default.is_a?(Proc)

Expand All @@ -34,13 +42,15 @@ def preference(name, type, options = {})
# cache_key will be nil for new objects, then if we check if there
# is a pending preference before going to default
define_method preference_getter_method(name) do
preferences.fetch(name) do
value = preferences.fetch(name) do
default.call
end
value = preference_encryptor.decrypt(value) if preference_encryptor.present?
value
end

define_method preference_setter_method(name) do |value|
value = convert_preference_value(value, type)
value = convert_preference_value(value, type, preference_encryptor)
preferences[name] = value

# If this is an activerecord object, we need to inform
Expand Down Expand Up @@ -72,6 +82,14 @@ def preference_type_getter_method(name)
"preferred_#{name}_type".to_sym
end

def preference_encryptor(options)
key = options[:encryption_key] ||
ENV['SOLIDUS_PREFERENCES_MASTER_KEY'] ||
Rails.application.credentials.secret_key_base

Spree::Encryptor.new(key)
end

# List of preference types allowed as form fields in the Solidus admin
#
# Overwrite this method in your class that includes +Spree::Preferable+
Expand Down
51 changes: 51 additions & 0 deletions core/spec/models/spree/preferences/preferable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,45 @@ def self.allowed_admin_form_preference_types
expect(@a.preferences[:product_attributes]).to eq({ id: 1, name: 2 })
end
end

context "converts encrypted_string preferences to encrypted values" do
it "with string, encryption key provided as option" do
A.preference :secret, :encrypted_string,
encryption_key: 'VkYp3s6v9y$B?E(H+MbQeThWmZq4t7w!'

@a.set_preference(:secret, 'secret_client_id')
expect(@a.get_preference(:secret)).to eq('secret_client_id')
expect(@a.preferences[:secret]).not_to eq('secret_client_id')
end

it "with string, encryption key provided as env variable" do
expect(ENV).to receive(:[]).with("SOLIDUS_PREFERENCES_MASTER_KEY").and_return("VkYp3s6v9y$B?E(H+MbQeThWmZq4t7w!")

A.preference :secret, :encrypted_string

@a.set_preference(:secret, 'secret_client_id')
expect(@a.get_preference(:secret)).to eq('secret_client_id')
expect(@a.preferences[:secret]).not_to eq('secret_client_id')
end

it "with string, encryption key provided as option, set using syntactic sugar method" do
A.preference :secret, :encrypted_string,
encryption_key: 'VkYp3s6v9y$B?E(H+MbQeThWmZq4t7w!'

@a.preferred_secret = 'secret_client_id'
expect(@a.preferred_secret).to eq('secret_client_id')
expect(@a.preferences[:secret]).not_to eq('secret_client_id')
end

it "with string, default value" do
A.preference :secret, :encrypted_string,
default: 'my_default_secret',
encryption_key: 'VkYp3s6v9y$B?E(H+MbQeThWmZq4t7w!'

expect(@a.get_preference(:secret)).to eq('my_default_secret')
expect(@a.preferences[:secret]).not_to eq('my_default_secret')
end
end
end

describe "persisted preferables" do
Expand All @@ -290,6 +329,7 @@ def self.down
class PrefTest < Spree::Base
preference :pref_test_pref, :string, default: 'abc'
preference :pref_test_any, :any, default: []
preference :pref_test_encrypted_string, :encrypted_string, encryption_key: 'VkYp3s6v9y$B?E(H+MbQeThWmZq4t7w!'
end
end

Expand Down Expand Up @@ -318,6 +358,17 @@ class PrefTest < Spree::Base
pr.save!
expect(pr.get_preference(:pref_test_any)).to eq([1, 2])
end

it "saves encrypted preferences for serialized object" do
pr = PrefTest.new
pr.set_preference(:pref_test_encrypted_string, 'secret_client_id')
expect(pr.get_preference(:pref_test_encrypted_string)).to eq('secret_client_id')
pr.save!
preferences_value_on_db = ActiveRecord::Base.connection.execute(
"SELECT preferences FROM pref_tests WHERE id=#{pr.id}"
).first
expect(preferences_value_on_db).not_to include('secret_client_id')
end
end

it "clear preferences when record is deleted" do
Expand Down

0 comments on commit e05da68

Please sign in to comment.