rails-pipeline 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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