diff options
3 files changed, 144 insertions, 39 deletions
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index 4c63c0dd629..01fb194281a 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -47,6 +47,14 @@ module TokenAuthenticatableStrategies options[:fallback] == true end + def migrating? + unless options[:migrating].in?([true, false, nil]) + raise ArgumentError, 'migrating: needs to be a boolean value!' + end + + options[:migrating] == true + end + def self.fabricate(model, field, options) if options[:digest] && options[:encrypted] raise ArgumentError, 'Incompatible options set!' diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb index c76cdc3bb90..4659109ca8f 100644 --- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb +++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb @@ -2,14 +2,24 @@ module TokenAuthenticatableStrategies class Encrypted < Base + def initialize(*) + super + + if migrating? && fallback? + raise ArgumentError, '`fallback` and `migration` options are not compatible!' + end + end + def find_token_authenticatable(token, unscoped = false) return unless token - encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) - token_authenticatable = relation(unscoped) - .find_by(encrypted_field => encrypted_value) + unless migrating? + encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) + token_authenticatable = relation(unscoped) + .find_by(encrypted_field => encrypted_value) + end - if fallback? + if migrating? || fallback? token_authenticatable ||= fallback_strategy .find_token_authenticatable(token) end @@ -39,6 +49,8 @@ module TokenAuthenticatableStrategies end def get_token(instance) + return fallback_strategy.get_token(instance) if migrating? + encrypted_token = instance.read_attribute(encrypted_field) token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) @@ -49,6 +61,7 @@ module TokenAuthenticatableStrategies raise ArgumentError unless token.present? instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) + instance[token_field] = token if migrating? instance[token_field] = nil if fallback? token end diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb index 4c074470f63..7dced6d79eb 100644 --- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' describe TokenAuthenticatableStrategies::Encrypted do let(:model) { double(:model) } let(:instance) { double(:instance) } - let(:options) { { fallback: true } } let(:encrypted) do Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value') @@ -13,60 +12,145 @@ describe TokenAuthenticatableStrategies::Encrypted do described_class.new(model, 'some_field', options) end + describe '.new' do + context 'when fallback and migration strategies are set' do + let(:options) { { fallback: true, migrating: true } } + + it 'raises an error' do + expect { subject }.to raise_error ArgumentError, /not compatible/ + end + end + end + describe '#find_token_authenticatable' do - it 'finds the encrypted resource by cleartext' do - allow(model).to receive(:find_by) - .with('some_field_encrypted' => encrypted) - .and_return('encrypted resource') + context 'when using fallback strategy' do + let(:options) { { fallback: true } } + + it 'finds the encrypted resource by cleartext' do + allow(model).to receive(:find_by) + .with('some_field_encrypted' => encrypted) + .and_return('encrypted resource') + + expect(subject.find_token_authenticatable('my-value')) + .to eq 'encrypted resource' + end - expect(subject.find_token_authenticatable('my-value')) - .to eq 'encrypted resource' + it 'uses fallback strategy when encrypted token cannot be found' do + allow(subject.send(:fallback_strategy)) + .to receive(:find_token_authenticatable) + .and_return('plaintext resource') + + allow(model).to receive(:find_by) + .with('some_field_encrypted' => encrypted) + .and_return(nil) + + expect(subject.find_token_authenticatable('my-value')) + .to eq 'plaintext resource' + end end - it 'uses fallback strategy when encrypted token cannot be found' do - allow(subject.send(:fallback_strategy)) - .to receive(:find_token_authenticatable) - .and_return('plaintext resource') + context 'when using migration strategy' do + let(:options) { { migrating: true } } + + it 'finds the cleartext resource by cleartext' do + allow(model).to receive(:find_by) + .with('some_field' => 'my-value') + .and_return('cleartext resource') - allow(model).to receive(:find_by) - .with('some_field_encrypted' => encrypted) - .and_return(nil) + expect(subject.find_token_authenticatable('my-value')) + .to eq 'cleartext resource' + end - expect(subject.find_token_authenticatable('my-value')) - .to eq 'plaintext resource' + it 'returns nil if resource cannot be found' do + allow(model).to receive(:find_by) + .with('some_field' => 'my-value') + .and_return(nil) + + expect(subject.find_token_authenticatable('my-value')) + .to be_nil + end end end describe '#get_token' do - it 'returns decrypted token when an encrypted token is present' do - allow(instance).to receive(:read_attribute) - .with('some_field_encrypted') - .and_return(encrypted) + context 'when using fallback strategy' do + let(:options) { { fallback: true } } + + it 'returns decrypted token when an encrypted token is present' do + allow(instance).to receive(:read_attribute) + .with('some_field_encrypted') + .and_return(encrypted) + + expect(subject.get_token(instance)).to eq 'my-value' + end - expect(subject.get_token(instance)).to eq 'my-value' + it 'returns the plaintext token when encrypted token is not present' do + allow(instance).to receive(:read_attribute) + .with('some_field_encrypted') + .and_return(nil) + + allow(instance).to receive(:read_attribute) + .with('some_field') + .and_return('cleartext value') + + expect(subject.get_token(instance)).to eq 'cleartext value' + end end - it 'returns the plaintext token when encrypted token is not present' do - allow(instance).to receive(:read_attribute) - .with('some_field_encrypted') - .and_return(nil) + context 'when using migration strategy' do + let(:options) { { migrating: true } } + + it 'returns cleartext token when an encrypted token is present' do + allow(instance).to receive(:read_attribute) + .with('some_field_encrypted') + .and_return(encrypted) + + allow(instance).to receive(:read_attribute) + .with('some_field') + .and_return('my-cleartext-value') + + expect(subject.get_token(instance)).to eq 'my-cleartext-value' + end + + it 'returns the cleartext token when encrypted token is not present' do + allow(instance).to receive(:read_attribute) + .with('some_field_encrypted') + .and_return(nil) - allow(instance).to receive(:read_attribute) - .with('some_field') - .and_return('cleartext value') + allow(instance).to receive(:read_attribute) + .with('some_field') + .and_return('cleartext value') - expect(subject.get_token(instance)).to eq 'cleartext value' + expect(subject.get_token(instance)).to eq 'cleartext value' + end end end describe '#set_token' do - it 'writes encrypted token and removes plaintext token and returns it' do - expect(instance).to receive(:[]=) - .with('some_field_encrypted', encrypted) - expect(instance).to receive(:[]=) - .with('some_field', nil) + context 'when using fallback strategy' do + let(:options) { { fallback: true } } + + it 'writes encrypted token and removes plaintext token and returns it' do + expect(instance).to receive(:[]=) + .with('some_field_encrypted', encrypted) + expect(instance).to receive(:[]=) + .with('some_field', nil) + + expect(subject.set_token(instance, 'my-value')).to eq 'my-value' + end + end + + context 'when using migration strategy' do + let(:options) { { migrating: true } } + + it 'writes encrypted token and writes plaintext token' do + expect(instance).to receive(:[]=) + .with('some_field_encrypted', encrypted) + expect(instance).to receive(:[]=) + .with('some_field', 'my-value') - expect(subject.set_token(instance, 'my-value')).to eq 'my-value' + expect(subject.set_token(instance, 'my-value')).to eq 'my-value' + end end end end |