summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Leitzen <pleitzen@gitlab.com>2019-08-30 14:46:28 +0200
committerPeter Leitzen <pleitzen@gitlab.com>2019-08-30 15:02:40 +0200
commitc77674e5f789c8e1158aa4f3a7356862b8461ad4 (patch)
treec6e0a4201c4d3ee8d8914a3f0c6752b78e206232
parent8d33fca8d0d17db330a4294bcba8f67ec105c759 (diff)
downloadgitlab-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.rb40
-rw-r--r--changelogs/unreleased/pl-encrypted-props.yml5
-rw-r--r--spec/models/service_spec.rb82
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