aws-sessionstore-dynamodb 0.5.0

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 (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