rails-pipeline 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +227 -0
  4. data/Rakefile +27 -0
  5. data/bin/pipeline +138 -0
  6. data/bin/redis-to-ironmq.rb +20 -0
  7. data/lib/rails-pipeline.rb +34 -0
  8. data/lib/rails-pipeline/emitter.rb +121 -0
  9. data/lib/rails-pipeline/handlers/activerecord_crud.rb +35 -0
  10. data/lib/rails-pipeline/handlers/base_handler.rb +19 -0
  11. data/lib/rails-pipeline/handlers/logger.rb +13 -0
  12. data/lib/rails-pipeline/ironmq_publisher.rb +37 -0
  13. data/lib/rails-pipeline/ironmq_pulling_subscriber.rb +96 -0
  14. data/lib/rails-pipeline/ironmq_subscriber.rb +21 -0
  15. data/lib/rails-pipeline/pipeline_version.rb +40 -0
  16. data/lib/rails-pipeline/protobuf/encrypted_message.pb.rb +37 -0
  17. data/lib/rails-pipeline/protobuf/encrypted_message.proto +18 -0
  18. data/lib/rails-pipeline/redis_forwarder.rb +207 -0
  19. data/lib/rails-pipeline/redis_ironmq_forwarder.rb +12 -0
  20. data/lib/rails-pipeline/redis_publisher.rb +71 -0
  21. data/lib/rails-pipeline/sns_publisher.rb +62 -0
  22. data/lib/rails-pipeline/subscriber.rb +185 -0
  23. data/lib/rails-pipeline/symmetric_encryptor.rb +127 -0
  24. data/lib/rails-pipeline/version.rb +3 -0
  25. data/lib/tasks/rails-pipeline_tasks.rake +4 -0
  26. data/spec/emitter_spec.rb +141 -0
  27. data/spec/handlers/activerecord_crud_spec.rb +100 -0
  28. data/spec/handlers/logger_spec.rb +42 -0
  29. data/spec/ironmp_pulling_subscriber_spec.rb +98 -0
  30. data/spec/ironmq_publisher_spec.rb +37 -0
  31. data/spec/pipeline_version_spec.rb +35 -0
  32. data/spec/redis_forwarder_spec.rb +99 -0
  33. data/spec/redis_publisher_spec.rb +36 -0
  34. data/spec/sns_publisher_spec.rb +28 -0
  35. data/spec/subscriber_spec.rb +278 -0
  36. data/spec/symmetric_encryptor_spec.rb +21 -0
  37. metadata +175 -0
@@ -0,0 +1,127 @@
1
+ # Mixin to enable symmetric encryption/decryption for pipeline, using protocol
2
+ # buffers on the wire.
3
+
4
+ require "rails-pipeline/protobuf/encrypted_message.pb"
5
+
6
+ module RailsPipeline
7
+
8
+ module SymmetricEncryptor
9
+ Error = Class.new(StandardError)
10
+ NoApiKeyError = Class.new(Error) do
11
+ def message
12
+ "You need to set the env variable PIPELINE_API_KEY to emit messages"
13
+ end
14
+ end
15
+
16
+ class << self
17
+ # Allow configuration via initializer
18
+ @@secret = nil
19
+ def _secret
20
+ @@secret.nil? ? ENV.fetch("PIPELINE_SECRET", Rails.application.config.secret_token) : @@secret
21
+ end
22
+
23
+ def secret=(secret)
24
+ @@secret = secret
25
+ end
26
+
27
+ @@api_key = nil
28
+ def _api_key
29
+ api_key = @@api_key.nil? ? ENV["PIPELINE_API_KEY"] : @@api_key
30
+ if api_key.blank?
31
+ raise NoApiKeyError.new
32
+ end
33
+ return api_key
34
+ end
35
+
36
+ def api_key=(api_key)
37
+ @@api_key = api_key
38
+ end
39
+ end
40
+
41
+ def self.included(base)
42
+ base.extend ClassMethods
43
+ end
44
+
45
+ module ClassMethods
46
+
47
+ def encrypt(plaintext, owner_info: nil, type_info: nil, topic: nil, event_type: nil)
48
+ # Inititalize a symmetric cipher for encryption
49
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
50
+ cipher.encrypt
51
+
52
+ # Create a random salt
53
+ salt = OpenSSL::Random.random_bytes(16)
54
+
55
+ # Create a PKCS5 key from the rails password
56
+ # NOTE: suggested way of doing this is by cipher.random_key
57
+ # and then we would store the key on the user.
58
+ key = _key(salt)
59
+
60
+ # Set the key and get a random initialization vector
61
+ cipher.key = key
62
+ iv = cipher.random_iv
63
+
64
+ # Do the encryption
65
+ ciphertext = cipher.update(plaintext) + cipher.final
66
+ uuid = SecureRandom.uuid
67
+ return RailsPipeline::EncryptedMessage.new(
68
+ uuid: uuid,
69
+ salt: Base64.encode64(salt),
70
+ iv: Base64.encode64(iv),
71
+ ciphertext: Base64.encode64(ciphertext),
72
+ owner_info: owner_info,
73
+ type_info: type_info,
74
+ topic: topic,
75
+ event_type: _event_type_value(event_type),
76
+ api_key: _api_key,
77
+ )
78
+ end
79
+
80
+ # Message is an instance of EncryptedMessage
81
+ def decrypt(message)
82
+ salt = Base64.decode64(message.salt)
83
+ key = _key(salt)
84
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
85
+ # Initialize for decryption
86
+ cipher.decrypt
87
+
88
+ # Set up key and iv
89
+ cipher.key = key
90
+ cipher.iv = Base64.decode64(message.iv)
91
+
92
+ # Decrypt
93
+ decoded = Base64.decode64(message.ciphertext)
94
+ plaintext = cipher.update(decoded) + cipher.final
95
+
96
+ return plaintext
97
+ end
98
+
99
+ def _secret
100
+ RailsPipeline::SymmetricEncryptor._secret
101
+ end
102
+
103
+ def _api_key
104
+ RailsPipeline::SymmetricEncryptor._api_key
105
+ end
106
+
107
+ def _key(salt)
108
+ iter = 10000
109
+ key_len = 32
110
+ key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(_secret, salt, iter, key_len)
111
+ return key
112
+ end
113
+
114
+ def _event_type_value(event_type)
115
+ case event_type
116
+ when :create
117
+ RailsPipeline::EncryptedMessage::EventType::CREATED
118
+ when :update
119
+ RailsPipeline::EncryptedMessage::EventType::UPDATED
120
+ when :destroy
121
+ RailsPipeline::EncryptedMessage::EventType::DELETED
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,3 @@
1
+ module RailsPipeline
2
+ VERSION = "1.1.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :rails-pipeline do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative 'pipeline_helper'
4
+
5
+
6
+ describe RailsPipeline::Emitter do
7
+ before do
8
+ @test_emitter = TestEmitter.new({foo: "bar"}, without_protection: true)
9
+ @test_model = TestModelWithTable.new
10
+ @default_emitter = DefaultEmitter.new({foo: "baz"}, without_protection: true)
11
+ TestModelWithTable.pipeline_method_cache = {}
12
+ TestEmitter.pipeline_method_cache = {}
13
+ DefaultEmitter.pipeline_method_cache = {}
14
+ end
15
+
16
+ it "should derive the topic name" do
17
+ TestEmitter.topic_name.should eql "harrys-#{Rails.env}-v1-test_emitters"
18
+ TestEmitter.topic_name("2_1").should eql "harrys-#{Rails.env}-v2-test_emitters"
19
+ TestEmitter.topic_name("2").should eql "harrys-#{Rails.env}-v2-test_emitters"
20
+ end
21
+
22
+ it "should detect all pipeline versions in class" do
23
+ pipeline_versions = TestEmitter.pipeline_versions
24
+ pipeline_versions.length.should eql 2
25
+ pipeline_versions.should include "1_1"
26
+ pipeline_versions.should include "2_0"
27
+
28
+ DefaultEmitter.pipeline_versions.length.should eql 1
29
+ DefaultEmitter.pipeline_versions.should eql ["1_0"]
30
+ end
31
+
32
+ context "with only default version" do
33
+ it "should produce all attributes" do
34
+ data = @default_emitter.to_pipeline_1_0
35
+ expect(data.foo).to eq("baz")
36
+ expect(data.class).to eq(DefaultEmitter_1_0)
37
+ end
38
+
39
+ it "should emit one version" do
40
+ @default_emitter.should_receive(:publish).once
41
+ @default_emitter.emit
42
+ end
43
+
44
+ it "should encrypt the payload" do
45
+ DefaultEmitter.should_receive(:encrypt).once { |data|
46
+ obj = DefaultEmitter_1_0.parse(data)
47
+ expect(obj.foo).to eq "baz"
48
+ }.and_call_original
49
+ @default_emitter.should_receive(:publish).once
50
+ @default_emitter.emit
51
+ end
52
+
53
+ it "should have the correct encrypted payload" do
54
+ DefaultEmitter.should_receive(:encrypt).once.and_call_original
55
+ # Just verify that the right encrypted data gets sent to publish
56
+ @default_emitter.should_receive(:publish).once do |topic, serialized_message|
57
+ topic.should eql "harrys-#{Rails.env}-v1-default_emitters"
58
+ message = RailsPipeline::EncryptedMessage.parse(serialized_message)
59
+ expect(message.type_info).to eq(DefaultEmitter_1_0.to_s)
60
+
61
+ plaintext = DefaultEmitter.decrypt(message)
62
+ obj = DefaultEmitter_1_0.parse(plaintext)
63
+ expect(obj.foo).to eq("baz")
64
+ end
65
+ @default_emitter.emit
66
+ end
67
+ end
68
+
69
+ context "with defined version" do
70
+ it "should produce expected version when called explicitly" do
71
+ data = @test_emitter.to_pipeline_1_1
72
+ data.foo.should eql "bar"
73
+ data.extrah.should eql "hi"
74
+ end
75
+
76
+ it "should emit multiple versions" do
77
+ @test_emitter.should_receive(:publish).twice
78
+ @test_emitter.emit
79
+ end
80
+ end
81
+
82
+ context 'event type' do
83
+ context 'created' do
84
+ it "sets event type correctly in enveloppe" do
85
+ @test_model.should_receive(:publish).once do |topic, serialized_message|
86
+ topic.should eq("harrys-#{Rails.env}-v1-test_model_with_tables")
87
+ message = RailsPipeline::EncryptedMessage.parse(serialized_message)
88
+ expect(message.event_type).to eq(RailsPipeline::EncryptedMessage::EventType::CREATED)
89
+ end
90
+ @test_model.save!
91
+ end
92
+ end
93
+
94
+ context 'updated' do
95
+ it "sets event type correctly in enveloppe" do
96
+ @test_model.save!
97
+ expect(@test_model).to receive(:publish).once do |topic, serialized_message|
98
+ expect(topic).to eq("harrys-#{Rails.env}-v1-test_model_with_tables")
99
+ message = RailsPipeline::EncryptedMessage.parse(serialized_message)
100
+ expect(message.event_type).to eq(RailsPipeline::EncryptedMessage::EventType::UPDATED)
101
+ end
102
+ @test_model.save!
103
+ end
104
+ end
105
+
106
+ context 'deleted' do
107
+ it "sets event type correctly in enveloppe" do
108
+ @test_model.save!
109
+ expect(@test_model).to receive(:publish).once do |topic, serialized_message|
110
+ expect(topic).to eq("harrys-#{Rails.env}-v1-test_model_with_tables")
111
+ message = RailsPipeline::EncryptedMessage.parse(serialized_message)
112
+ expect(message.event_type).to eq(RailsPipeline::EncryptedMessage::EventType::DELETED)
113
+ end
114
+ @test_model.destroy
115
+ end
116
+ end
117
+ end
118
+
119
+ context 'methods cache' do
120
+ context 'empty cache' do
121
+ before { @test_model.save! }
122
+ it 'caches the pipeline versions' do
123
+ TestModelWithTable.pipeline_method_cache[RailsPipeline::PipelineVersion.new('1_1')].should eq(:to_pipeline_1_1)
124
+ end
125
+ end
126
+
127
+ context 'non empty cache' do
128
+
129
+ before do
130
+ # warms the cache
131
+ TestModelWithTable.pipeline_versions
132
+ end
133
+ it "reads from cache" do
134
+ version = RailsPipeline::PipelineVersion.new('1_1')
135
+ TestModelWithTable.should_not_receive(:instance_methods)
136
+ # TestModelWithTable.pipeline_method_cache.should_receive(:[]).with(version).once.and_call_original
137
+ @test_model.save!
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+ require 'pipeline_helper'
3
+
4
+ describe RailsPipeline::SubscriberHandler::ActiveRecordCRUD do
5
+ describe 'handle payload event type' do
6
+ let(:handler) {
7
+ RailsPipeline::SubscriberHandler::ActiveRecordCRUD.new(
8
+ payload, target_class: subscriber.target_class(payload), event_type: event)
9
+ }
10
+ let(:subscriber) { TestSubscriber.new }
11
+ let(:test_message) { test_model.create_message("1_1", event) }
12
+ let(:payload_str) { subscriber.class.decrypt(test_message) }
13
+ let(:clazz) { Object.const_get(test_message.type_info) }
14
+ let(:payload) { clazz.parse(payload_str) }
15
+
16
+ before(:each) do
17
+ RailsPipeline::Subscriber.register(TestEmitter_1_1, TestModelWithTable)
18
+ end
19
+
20
+ after(:each) { TestModelWithTable.delete_all }
21
+
22
+ context 'CREATED' do
23
+ let(:event) { RailsPipeline::EncryptedMessage::EventType::CREATED }
24
+ let(:test_model) { TestModelWithTable.new({id: 42, foo: 'bar'}, without_protection: true) }
25
+
26
+ it "should create an object" do
27
+ expect {
28
+ handler.handle_payload
29
+ }.to change(TestModelWithTable, :count).by(1)
30
+ end
31
+
32
+ it "should create with correct attributes" do
33
+ new_object = handler.handle_payload
34
+ new_object.should be_persisted
35
+ new_object.id.should eq(42)
36
+ new_object.foo.should eq('bar')
37
+ end
38
+
39
+ it "should log if creation failed" do
40
+ TestModelWithTable.create({id: 42}, without_protection: true)
41
+ RailsPipeline.logger.should_receive(:error).with("Could not handle payload: #{payload.inspect}, event_type: #{event}")
42
+ handler.handle_payload
43
+ end
44
+ end
45
+
46
+ context 'UPDATED' do
47
+ let(:event) { RailsPipeline::EncryptedMessage::EventType::UPDATED }
48
+ let(:test_model) { TestModelWithTable.new({id: 42, foo: 'bar'}, without_protection: true) }
49
+
50
+ it "should update existing object" do
51
+ test_model.save!
52
+ test_model.foo = 'qux'
53
+ object = handler.handle_payload
54
+ object.should be_persisted
55
+ object.id.should eq(42)
56
+ object.foo.should eq('qux')
57
+ end
58
+
59
+ it "should log if update failed" do
60
+ RailsPipeline.logger.should_receive(:error).with("Could not handle payload: #{payload.inspect}, event_type: #{event}")
61
+ handler.handle_payload
62
+ end
63
+ end
64
+
65
+ context 'DELETED' do
66
+ let(:event) { RailsPipeline::EncryptedMessage::EventType::DELETED }
67
+ let(:test_model) { TestModelWithTable.new({id: 42, foo: 'bar'}, without_protection: true) }
68
+
69
+ it "should update existing object" do
70
+ test_model.save!
71
+ object = handler.handle_payload
72
+ object.should be_destroyed
73
+ end
74
+
75
+ it "should log if update failed" do
76
+ RailsPipeline.logger.should_receive(:error).with("Could not handle payload: #{payload.inspect}, event_type: #{event}")
77
+ handler.handle_payload
78
+ end
79
+ end
80
+ end
81
+
82
+ describe 'attributes' do
83
+ let(:handler) {
84
+ RailsPipeline::SubscriberHandler::ActiveRecordCRUD.new(
85
+ payload, target_class: subscriber.target_class(payload), event_type: event)
86
+ }
87
+ let(:subscriber) { TestSubscriber.new }
88
+ let(:test_model) { TestModelWithTable.new({foo: 'bar'}, without_protection: true) }
89
+ let(:test_message) { test_model.create_message("1_1", event) }
90
+ let(:payload_str) { subscriber.class.decrypt(test_message) }
91
+ let(:clazz) { Object.const_get(test_message.type_info) }
92
+ let(:payload) { clazz.parse(payload_str) }
93
+ let(:event) { RailsPipeline::EncryptedMessage::EventType::CREATED }
94
+
95
+ it 'converts datetime correctly' do
96
+ test_model.save!
97
+ handler._attributes(payload)[:created_at].should be_an_instance_of(DateTime)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'pipeline_helper'
3
+
4
+ describe RailsPipeline::SubscriberHandler::Logger do
5
+ describe 'handle payload event type' do
6
+ let(:handler) {
7
+ RailsPipeline::SubscriberHandler::Logger.new(
8
+ payload, envelope: test_message)
9
+ }
10
+ let(:test_model) { TestModelWithTable.new({id: 42, foo: 'bar'}, without_protection: true) }
11
+ let(:subscriber) { TestSubscriber.new }
12
+ let(:test_message) { test_model.create_message("1_1", event) }
13
+ let(:payload_str) { subscriber.class.decrypt(test_message) }
14
+ let(:clazz) { Object.const_get(test_message.type_info) }
15
+ let(:payload) { clazz.parse(payload_str) }
16
+
17
+ context 'CREATED' do
18
+ let(:event) { RailsPipeline::EncryptedMessage::EventType::CREATED }
19
+
20
+ it 'logs everything' do
21
+ expect(RailsPipeline.logger).to receive(:info).with(test_message.to_s)
22
+ handler.handle_payload
23
+ end
24
+ end
25
+
26
+ context 'UPDATED' do
27
+ let(:event) { RailsPipeline::EncryptedMessage::EventType::UPDATED }
28
+ it 'logs everything' do
29
+ expect(RailsPipeline.logger).to receive(:info).with(test_message.to_s)
30
+ handler.handle_payload
31
+ end
32
+ end
33
+
34
+ context 'DELETED' do
35
+ let(:event) { RailsPipeline::EncryptedMessage::EventType::DELETED }
36
+ it 'logs everything' do
37
+ expect(RailsPipeline.logger).to receive(:info).with(test_message.to_s)
38
+ handler.handle_payload
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+
3
+ describe RailsPipeline::IronmqPullingSubscriber do
4
+ let(:subject){RailsPipeline::IronmqPullingSubscriber.new("test_queue")}
5
+ let(:failing_proc){Proc.new{false}}
6
+ let(:successful_proc){Proc.new{true}}
7
+
8
+ describe "#start_subscription" do
9
+ it "should attempt to process a dequeued message" do
10
+ subject.stub(:pull_message).and_yield("foo")
11
+ subject.stub(:process_message){subject.deactivate_subscription}
12
+ subject.start_subscription{}
13
+ expect(subject).to have_received(:process_message)
14
+ end
15
+ end
16
+
17
+ describe "#process_message" do
18
+ context "when receiving a nil message" do
19
+ it "deactivates the current subscription" do
20
+ subject.process_message(nil, true, successful_proc)
21
+ expect(subject.active_subscription?).to eql false
22
+ end
23
+ end
24
+
25
+ context "when receiving a non-nil message" do
26
+ context "when an issue occurs while generating an message envelope" do
27
+ let(:malformed_message){double("message", :body => "a malformed message",
28
+ :delete => "a fine deletion implementation")}
29
+
30
+ before(:each) do
31
+ subject.activate_subscription
32
+ expect {subject.process_message(malformed_message, true, successful_proc)}.to raise_error
33
+ end
34
+
35
+ it "does not delete the message" do
36
+ expect(malformed_message).to_not have_received(:delete)
37
+ end
38
+
39
+ it "deactivates the subscription" do
40
+ expect(subject.active_subscription?).to eql false
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "#process_envelope" do
47
+ let(:failing_proc){Proc.new{false}}
48
+ let(:successful_proc){Proc.new{true}}
49
+ let(:test_envelope){double("envelope")}
50
+ let(:message){double("message", :delete => "a deletion implementation")}
51
+
52
+ context "when the block passed returns true" do
53
+ it "deletes the passed message" do
54
+ subject.process_envelope(test_envelope, message, successful_proc)
55
+ expect(message).to have_received(:delete)
56
+ end
57
+ end
58
+
59
+ context "when the block passeed returns false" do
60
+ it "does not delete the passed message" do
61
+ subject.process_envelope(test_envelope, message, failing_proc)
62
+ expect(message).to_not have_received(:delete)
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "#active_subscription?" do
68
+ context "when the subscriber is currently active" do
69
+ it "returns true" do
70
+ subject.activate_subscription
71
+ expect(subject.active_subscription?).to eq true
72
+ end
73
+ end
74
+
75
+ context "when the subscriber is not currently active" do
76
+ it "returns false" do
77
+ subject.deactivate_subscription
78
+ expect(subject.active_subscription?).to eq false
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "activate_subscription" do
84
+ it "sets the subscription status of the subscriber to true" do
85
+ subject.deactivate_subscription
86
+ subject.activate_subscription
87
+ expect(subject.active_subscription?).to eql true
88
+ end
89
+ end
90
+
91
+ describe "deactivate_subscription" do
92
+ it "sets the subscription status of the subscriber to false" do
93
+ subject.activate_subscription
94
+ subject.deactivate_subscription
95
+ expect(subject.active_subscription?).to eql false
96
+ end
97
+ end
98
+ end