aws-sessionstore-dynamodb 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.yardopts +3 -0
  4. data/Gemfile +28 -0
  5. data/LICENSE.txt +12 -0
  6. data/README.md +171 -0
  7. data/Rakefile +15 -0
  8. data/aws-sessionstore-dynamodb.gemspec +18 -0
  9. data/lib/aws-sessionstore-dynamodb.rb +34 -0
  10. data/lib/aws/session_store/dynamo_db/configuration.rb +298 -0
  11. data/lib/aws/session_store/dynamo_db/errors/base_handler.rb +45 -0
  12. data/lib/aws/session_store/dynamo_db/errors/default_handler.rb +57 -0
  13. data/lib/aws/session_store/dynamo_db/garbage_collection.rb +128 -0
  14. data/lib/aws/session_store/dynamo_db/invalid_id_error.rb +21 -0
  15. data/lib/aws/session_store/dynamo_db/lock_wait_timeout_error.rb +21 -0
  16. data/lib/aws/session_store/dynamo_db/locking/base.rb +162 -0
  17. data/lib/aws/session_store/dynamo_db/locking/null.rb +40 -0
  18. data/lib/aws/session_store/dynamo_db/locking/pessimistic.rb +160 -0
  19. data/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb +21 -0
  20. data/lib/aws/session_store/dynamo_db/rack_middleware.rb +130 -0
  21. data/lib/aws/session_store/dynamo_db/railtie.rb +28 -0
  22. data/lib/aws/session_store/dynamo_db/table.rb +98 -0
  23. data/lib/aws/session_store/dynamo_db/tasks/session_table.rake +21 -0
  24. data/lib/aws/session_store/dynamo_db/version.rb +21 -0
  25. data/lib/rails/generators/sessionstore/dynamodb/dynamodb_generator.rb +55 -0
  26. data/lib/rails/generators/sessionstore/dynamodb/templates/sessionstore/USAGE +13 -0
  27. data/lib/rails/generators/sessionstore/dynamodb/templates/sessionstore/dynamodb.yml +71 -0
  28. data/lib/rails/generators/sessionstore/dynamodb/templates/sessionstore_migration.rb +10 -0
  29. data/spec/aws/session_store/dynamo_db/app_config.yml +19 -0
  30. data/spec/aws/session_store/dynamo_db/config/dynamo_db_session.yml +24 -0
  31. data/spec/aws/session_store/dynamo_db/configuration_spec.rb +101 -0
  32. data/spec/aws/session_store/dynamo_db/error/default_error_handler_spec.rb +62 -0
  33. data/spec/aws/session_store/dynamo_db/garbage_collection_spec.rb +156 -0
  34. data/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb +95 -0
  35. data/spec/aws/session_store/dynamo_db/rack_middleware_database_spec.rb +129 -0
  36. data/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb +149 -0
  37. data/spec/aws/session_store/dynamo_db/rails_app_config.yml +24 -0
  38. data/spec/aws/session_store/dynamo_db/table_spec.rb +46 -0
  39. data/spec/spec_helper.rb +61 -0
  40. data/tasks/test.rake +29 -0
  41. metadata +123 -0
@@ -0,0 +1,95 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+
15
+ require 'spec_helper'
16
+
17
+ describe AWS::SessionStore::DynamoDB::RackMiddleware do
18
+ include Rack::Test::Methods
19
+
20
+ def thread(mul_val, time, check)
21
+ Thread.new do
22
+ sleep(time)
23
+ get "/"
24
+ last_request.session[:multiplier].should eq(mul_val) if check
25
+ end
26
+ end
27
+
28
+ def thread_exception(error)
29
+ Thread.new { expect { get "/" }.to raise_error(error) }
30
+ end
31
+
32
+ def update_item_mock(options, update_method)
33
+ if options[:return_values] == "UPDATED_NEW" && options.has_key?(:expected)
34
+ sleep(0.50)
35
+ update_method.call(options)
36
+ else
37
+ update_method.call(options)
38
+ end
39
+ end
40
+
41
+ let(:base_app) { MultiplierApplication.new }
42
+ let(:app) { AWS::SessionStore::DynamoDB::RackMiddleware.new(base_app, @options) }
43
+
44
+ context "Mock Multiple Threaded Sessions", :integration => true do
45
+ before do
46
+ @options = AWS::SessionStore::DynamoDB::Configuration.new.to_hash
47
+ @options[:enable_locking] = true
48
+ @options[:secret_key] = 'watermelon_smiles'
49
+
50
+ update_method = @options[:dynamo_db_client].method(:update_item)
51
+ @options[:dynamo_db_client].should_receive(:update_item).at_least(:once) do |options|
52
+ update_item_mock(options, update_method)
53
+ end
54
+ end
55
+
56
+ it "should wait for lock" do
57
+ @options[:lock_expiry_time] = 2000
58
+
59
+ get "/"
60
+ last_request.session[:multiplier].should eq(1)
61
+
62
+ t1 = thread(2, 0, false)
63
+ t2 = thread(4, 0.25, true)
64
+ t1.join
65
+ t2.join
66
+ end
67
+
68
+ it "should bust lock" do
69
+ @options[:lock_expiry_time] = 100
70
+
71
+ get "/"
72
+ last_request.session[:multiplier].should eq(1)
73
+
74
+ t1 = thread_exception(AWS::DynamoDB::Errors::ConditionalCheckFailedException)
75
+ t2 = thread(2, 0.25, true)
76
+ t1.join
77
+ t2.join
78
+ end
79
+
80
+ it "should throw exceeded time spent aquiring lock error" do
81
+ @options[:lock_expiry_time] = 1000
82
+ @options[:lock_retry_delay] = 100
83
+ @options[:lock_max_wait_time] = 0.25
84
+
85
+ get "/"
86
+ last_request.session[:multiplier].should eq(1)
87
+
88
+ t1 = thread(2, 0, false)
89
+ sleep(0.25)
90
+ t2 = thread_exception(AWS::SessionStore::DynamoDB::LockWaitTimeoutError)
91
+ t1.join
92
+ t2.join
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,129 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'spec_helper'
15
+
16
+ module AWS
17
+ module SessionStore
18
+ module DynamoDB
19
+ describe RackMiddleware do
20
+ include Rack::Test::Methods
21
+
22
+ instance_exec(&ConstantHelpers)
23
+
24
+ before do
25
+ @options = { :secret_key => 'watermelon_cherries' }
26
+ end
27
+
28
+ # Table options for client
29
+ def table_opts(sid)
30
+ {
31
+ :table_name => Configuration::DEFAULTS[:table_name],
32
+ :key => { Configuration::DEFAULTS[:table_key] => { :s => sid } }
33
+ }
34
+ end
35
+
36
+ # Attributes to be retrieved via client
37
+ def attr_opts
38
+ {
39
+ :attributes_to_get => ["data", "created_at", "locked_at"],
40
+ :consistent_read => true
41
+ }
42
+ end
43
+
44
+ def extract_time(sid)
45
+ options = table_opts(sid).merge(attr_opts)
46
+ Time.at((client.get_item(options)[:item]["created_at"][:n]).to_f)
47
+ end
48
+
49
+ let(:base_app) { MultiplierApplication.new }
50
+ let(:app) { RackMiddleware.new(base_app, @options) }
51
+ let(:config) { Configuration.new }
52
+ let(:client) { config.dynamo_db_client }
53
+
54
+ context "Testing best case session storage", :integration => true do
55
+ it "stores session data in session object" do
56
+ get "/"
57
+ last_request.session[:multiplier].should eq(1)
58
+ end
59
+
60
+ it "creates a new HTTP cookie when Cookie not supplied" do
61
+ get "/"
62
+ last_response.body.should eq('All good!')
63
+ last_response['Set-Cookie'].should be_true
64
+ end
65
+
66
+ it "does not rewrite Cookie if cookie previously/accuarately set" do
67
+ get "/"
68
+ last_response['Set-Cookie'].should_not be_nil
69
+
70
+
71
+ get "/"
72
+ last_response['Set-Cookie'].should be_nil
73
+ end
74
+
75
+ it "does not set cookie when defer option is specifed" do
76
+ @options[:defer] = true
77
+ get "/"
78
+ last_response['Set-Cookie'].should be_nil
79
+ end
80
+
81
+ it "creates new sessopm with false/nonexistant http-cookie id" do
82
+ get "/", {}, invalid_cookie.merge(invalid_session_data)
83
+ last_response['Set-Cookie'].should_not eq("rack.session=ApplePieBlueberries")
84
+ last_response['Set-Cookie'].should_not be_nil
85
+ end
86
+
87
+ it "expires after specified time and sets date for cookie to expire" do
88
+ @options[:expire_after] = 1
89
+ get "/"
90
+ session_cookie = last_response['Set-Cookie']
91
+ sleep(1.2)
92
+
93
+ get "/"
94
+ last_response['Set-Cookie'].should_not be_nil
95
+ last_response['Set-Cookie'].should_not eq(session_cookie)
96
+ end
97
+
98
+ it "will not set a session cookie when defer is true" do
99
+ @options[:defer] = true
100
+ get "/"
101
+ last_response['Set-Cookie'].should eq(nil)
102
+ end
103
+
104
+ it "adds the created at attribute for a new session" do
105
+ get "/"
106
+ last_request.env["dynamo_db.new_session"].should eq("true")
107
+ sid = last_response['Set-Cookie'].split(/[;\=]/)[1]
108
+ time = extract_time(sid)
109
+ time.should be_within(2).of(Time.now)
110
+
111
+ get "/"
112
+ last_request.env['dynamo_db.new_session'].should be(nil)
113
+ end
114
+
115
+ it "releases pessimistic lock at finish of transaction" do
116
+ @options[:enable_locking] = true
117
+ get "/"
118
+ last_request.env["dynamo_db.new_session"].should eq("true")
119
+ sid = last_response['Set-Cookie'].split(/[;\=]/)[1]
120
+
121
+ get "/"
122
+ options = table_opts(sid).merge(attr_opts)
123
+ client.get_item(options)[:item]["locked_at"].should be_nil
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,149 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'spec_helper'
15
+
16
+ module AWS
17
+ module SessionStore
18
+ module DynamoDB
19
+ describe RackMiddleware do
20
+ include Rack::Test::Methods
21
+
22
+ before { @options = {} }
23
+
24
+ def ensure_data_updated(mutated_data)
25
+ dynamo_db_client.should_receive(:update_item) do |options|
26
+ if mutated_data
27
+ options[:attribute_updates]["data"].should_not be_nil
28
+ else
29
+ options[:attribute_updates]["data"].should be_nil
30
+ end
31
+ end
32
+ end
33
+
34
+ before do
35
+ @options = {
36
+ :dynamo_db_client => dynamo_db_client,
37
+ :secret_key => 'watermelon_cherries'
38
+ }
39
+ end
40
+
41
+ let(:base_app) { MultiplierApplication.new }
42
+ let(:app) { RackMiddleware.new(base_app, @options) }
43
+
44
+ let(:sample_packed_data) do
45
+ [Marshal.dump("multiplier" => 1)].pack("m*")
46
+ end
47
+
48
+ let(:dynamo_db_client) do
49
+ client = double('AWS::DynamoDB::Client')
50
+ client.stub(:delete_item) { 'Deleted' }
51
+ client.stub(:list_tables) { {:table_names => ['Sessions']} }
52
+ client.stub(:get_item) do
53
+ { :item => { 'data' => { :s => sample_packed_data } } }
54
+ end
55
+ client.stub(:update_item) do
56
+ { :attributes => { :created_at => 'now' } }
57
+ end
58
+ client
59
+ end
60
+
61
+ context "Testing best case session storage with mock client" do
62
+ it "stores session data in session object" do
63
+ get "/"
64
+ last_request.session.to_hash.should eq("multiplier" => 1)
65
+ end
66
+
67
+ it "creates a new HTTP cookie when Cookie not supplied" do
68
+ get "/"
69
+ last_response.body.should eq('All good!')
70
+ last_response['Set-Cookie'].should be_true
71
+ end
72
+
73
+ it "loads/manipulates a session based on id from HTTP-Cookie" do
74
+ get "/"
75
+ last_request.session.to_hash.should eq("multiplier" => 1)
76
+
77
+ get "/"
78
+ last_request.session.to_hash.should eq("multiplier" => 2)
79
+ end
80
+
81
+ it "does not rewrite Cookie if cookie previously/accuarately set" do
82
+ get "/"
83
+ last_response['Set-Cookie'].should_not be_nil
84
+
85
+ get "/"
86
+ last_response['Set-Cookie'].should be_nil
87
+ end
88
+
89
+ it "does not set cookie when defer option is specifed" do
90
+ @options[:defer] = true
91
+ get "/"
92
+ last_response['Set-Cookie'].should eq(nil)
93
+ end
94
+
95
+ it "creates new sessopm with false/nonexistant http-cookie id" do
96
+ get "/"
97
+ last_response['Set-Cookie'].should_not eq('1234')
98
+ last_response['Set-Cookie'].should_not be_nil
99
+ end
100
+
101
+ it "expires after specified time and sets date for cookie to expire" do
102
+ @options[:expire_after] = 0
103
+ get "/"
104
+ session_cookie = last_response['Set-Cookie']
105
+
106
+ get "/"
107
+ last_response['Set-Cookie'].should_not be_nil
108
+ last_response['Set-Cookie'].should_not eq(session_cookie)
109
+ end
110
+
111
+ it "doesn't reset Cookie if not outside expire date" do
112
+ @options[:expire_after] = 3600
113
+ get "/"
114
+ session_cookie = last_response['Set-Cookie']
115
+ get "/"
116
+ last_response['Set-Cookie'].should eq(session_cookie)
117
+ end
118
+
119
+ it "will not set a session cookie when defer is true" do
120
+ @options[:defer] = true
121
+ get "/"
122
+ last_response['Set-Cookie'].should eq(nil)
123
+ end
124
+
125
+ it "generates sid and migrates data to new sid when renew is selected" do
126
+ @options[:renew] = true
127
+ get "/"
128
+ last_request.session.to_hash.should eq("multiplier" => 1)
129
+ session_cookie = last_response['Set-Cookie']
130
+
131
+ get "/" , "HTTP_Cookie" => session_cookie
132
+ last_response['Set-Cookie'].should_not eq(session_cookie)
133
+ last_request.session.to_hash.should eq("multiplier" => 2)
134
+ session_cookie = last_response['Set-Cookie']
135
+ end
136
+
137
+ it "doesn't resend unmutated data" do
138
+ ensure_data_updated(true)
139
+ @options[:renew] = true
140
+ get "/"
141
+
142
+ ensure_data_updated(false)
143
+ get "/", {}, { "rack.session" => { "multiplier" => nil } }
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,24 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ development:
15
+ key1: development value 1
16
+ test:
17
+ table_name: NewTable
18
+ table_key: Somekey
19
+ consistent_read: true
20
+ AWS_ACCESS_KEY_ID: FakeKey
21
+ AWS_SECRET_ACCESS_KEY: Secret
22
+ AWS_REGION: New York
23
+ production:
24
+ key1: production value 1
@@ -0,0 +1,46 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'spec_helper'
15
+ require 'stringio'
16
+ require 'logger'
17
+
18
+ module AWS
19
+ module SessionStore
20
+ module DynamoDB
21
+ describe Table do
22
+ context "Mock Table Methods Tests", :integration => true do
23
+ let(:table_name) { "sessionstore-integration-test-#{Time.now.to_i}" }
24
+ let(:options) { {:table_name => table_name} }
25
+ let(:io) { StringIO.new }
26
+
27
+ before { Table.stub(:logger) { Logger.new(io) } }
28
+
29
+ it "Creates and deletes a new table" do
30
+ Table.create_table(options)
31
+
32
+ # second attempt should warn
33
+ Table.create_table(options)
34
+
35
+ io.string.should include("Table #{table_name} created, waiting for activation...\n")
36
+ io.string.should include("Table #{table_name} is now ready to use.\n")
37
+ io.string.should include("Table #{table_name} already exists, skipping creation.\n")
38
+
39
+ # now delete table
40
+ Table.delete_table(options)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,61 @@
1
+ # Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ begin
15
+ if ENV['COVERAGE']
16
+ require 'simplecov'
17
+ SimpleCov.start { add_filter 'spec' }
18
+ end
19
+ rescue LoadError
20
+ end
21
+
22
+ $: << File.join(File.dirname(File.dirname(__FILE__)), "lib")
23
+
24
+ require 'rspec'
25
+ require 'aws-sessionstore-dynamodb'
26
+ require 'rack/test'
27
+
28
+ # Default Rack application
29
+ class MultiplierApplication
30
+ def call(env)
31
+ if env['rack.session'][:multiplier]
32
+ env['rack.session'][:multiplier] *= 2
33
+ else
34
+ env['rack.session'][:multiplier] = 1
35
+ end
36
+ [200, {'Content-Type' => 'text/plain'}, ['All good!']]
37
+ end
38
+ end
39
+
40
+ ConstantHelpers = lambda do
41
+ let(:token_error_msg) { 'The security token included in the request is invalid' }
42
+ let(:resource_error) { AWS::DynamoDB::Errors::ResourceNotFoundException }
43
+ let(:key_error) { AWS::DynamoDB::Errors::ValidationException.new(key_error_msg) }
44
+ let(:key_error_msg) { 'The provided key element does not match the schema' }
45
+ let(:client_error) { AWS::DynamoDB::Errors::UnrecognizedClientException }
46
+ let(:invalid_cookie) { {"HTTP_COOKIE" => "rack.session=ApplePieBlueberries"} }
47
+ let(:invalid_session_data) { {"rack.session"=>{"multiplier" => 1}} }
48
+ let(:rack_default_error_msg) { "Warning! AWS::SessionStore::DynamoDB failed to save session. Content dropped.\n" }
49
+ let(:missing_key_error) { AWS::SessionStore::DynamoDB::MissingSecretKeyError }
50
+ end
51
+
52
+ RSpec.configure do |c|
53
+ c.before(:each, :integration => true) do
54
+ opts = {:table_name => 'sessionstore-integration-test'}
55
+
56
+ defaults = AWS::SessionStore::DynamoDB::Configuration::DEFAULTS
57
+ defaults = defaults.merge(opts)
58
+ stub_const("AWS::SessionStore::DynamoDB::Configuration::DEFAULTS", defaults)
59
+ AWS::SessionStore::DynamoDB::Table.create_table(opts)
60
+ end
61
+ end