diff options
author | Peter Leitzen <pleitzen@gitlab.com> | 2019-08-30 14:46:28 +0200 |
---|---|---|
committer | Peter Leitzen <pleitzen@gitlab.com> | 2019-08-30 15:02:40 +0200 |
commit | c77674e5f789c8e1158aa4f3a7356862b8461ad4 (patch) | |
tree | c6e0a4201c4d3ee8d8914a3f0c6752b78e206232 | |
parent | 8d33fca8d0d17db330a4294bcba8f67ec105c759 (diff) | |
download | gitlab-ce-c77674e5f789c8e1158aa4f3a7356862b8461ad4.tar.gz |
Support encrypted properties for project services
Previously, we stored sensitive data either as plain text or
created separate models utilizing `attr_encrypted`.
This commit brings `attr_encrypted` functionality into project services!
-rw-r--r-- | app/models/service.rb | 40 | ||||
-rw-r--r-- | changelogs/unreleased/pl-encrypted-props.yml | 5 | ||||
-rw-r--r-- | spec/models/service_spec.rb | 82 |
3 files changed, 126 insertions, 1 deletions
diff --git a/app/models/service.rb b/app/models/service.rb index 431c5881460..fa0a84982ef 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -177,7 +177,7 @@ class Service < ApplicationRecord class_eval <<~RUBY, __FILE__, __LINE__ + 1 unless method_defined?(arg) def #{arg} - properties['#{arg}'] + properties['#{arg}'] if properties end end @@ -218,6 +218,44 @@ class Service < ApplicationRecord end end + def self.prop_accessor_encrypted(*args) + encrypted_args = args.flat_map do |arg| + ["encrypted_#{arg}", "encrypted_#{arg}_iv"] + end + prop_accessor(*encrypted_args) + + encryption_options = { + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' + } + attr_encrypted(*args, encryption_options) + + args.each do |arg| + class_eval <<~RUBY, __FILE__, __LINE__ + 1 + alias_method :_old_prop_accessor_encrypted_#{arg}=, :#{arg}= + undef_method :#{arg}= # avoid warnings of method redefintions + + def #{arg}=(value) + updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? + self._old_prop_accessor_encrypted_#{arg} = value + end + + def #{arg}_changed? + #{arg}_touched? && #{arg} != #{arg}_was + end + + def #{arg}_touched? + updated_properties.include?('#{arg}') + end + + def #{arg}_was + updated_properties['#{arg}'] + end + RUBY + end + end + # Returns a hash of the properties that have been assigned a new value since last save, # indicating their original values (attr => original value). # ActiveRecord does not provide a mechanism to track changes in serialized keys, diff --git a/changelogs/unreleased/pl-encrypted-props.yml b/changelogs/unreleased/pl-encrypted-props.yml new file mode 100644 index 00000000000..4b39d90f5bd --- /dev/null +++ b/changelogs/unreleased/pl-encrypted-props.yml @@ -0,0 +1,5 @@ +--- +title: Support encrypted properties for project services +merge_request: 32427 +author: +type: added diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0797b9a9d83..199d9ff2821 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -262,6 +262,88 @@ describe Service do end end + describe '.prop_accessor_encrypted' do + class EncryptedService < Service + prop_accessor_encrypted :token, :secret + end + + let(:described_class) { EncryptedService } + let(:project) { create(:project) } + let(:loaded) { described_class.find(service.id) } + + let(:service) do + described_class.create!(project: project, **properties) + end + + context 'with properties changes' do + let(:token_value) { 'token' } + let(:secret_value) { 'secret' } + + let(:properties) do + { token: token_value, secret: secret_value } + end + + it 'assigns attributes' do + expect(service.token).to eq(token_value) + expect(service.secret).to eq(secret_value) + end + + it 'stores properties encrypted' do + expect(service.properties.values).not_to include( + token_value, secret_value + ) + end + + it 'decrypts properly after loading' do + expect(loaded.token).to eq(token_value) + expect(loaded.secret).to eq(secret_value) + end + + context 'when changed' do + let(:new_value) { 'changed' } + + before do + service.token = new_value + end + + it 'tracks changes, touches and was' do + expect(service.token).to eq(new_value) + expect(service.token_changed?).to eq(true) + expect(service.token_touched?).to eq(true) + expect(service.token_was).to eq(token_value) + + expect(service.secret).to eq(secret_value) + expect(service.secret_changed?).to eq(false) + expect(service.secret_touched?).to eq(false) + expect(service.secret_was).to eq(nil) + end + end + + context 'when touched' do + before do + service.token = token_value + end + + it 'tracks changes, touches and was' do + expect(service.token).to eq(token_value) + expect(service.token_changed?).to eq(false) + expect(service.token_touched?).to eq(true) + expect(service.token_was).to eq(token_value) + end + end + end + + context 'no property changes' do + let(:properties) { {} } + + it 'properties are nil' do + expect(service.token).to be_nil + expect(service.secret).to be_nil + expect(service.properties).to be_empty + end + end + end + describe "callbacks" do let(:project) { create(:project) } let!(:service) do |