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,160 @@
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
+ module AWS::SessionStore::DynamoDB::Locking
16
+ # This class implements a pessimistic locking strategy for the
17
+ # DynamoDB session handler. Sessions obtain an exclusive lock
18
+ # for reads that is only released when the session is saved.
19
+ class Pessimistic < AWS::SessionStore::DynamoDB::Locking::Base
20
+ WAIT_ERROR =
21
+
22
+ # Saves the session.
23
+ def set_session_data(env, sid, session, options = {})
24
+ super(env, sid, session, set_lock_options(env, options))
25
+ end
26
+
27
+ # Gets session from database and places a lock on the session
28
+ # while you are reading from the database.
29
+ def get_session_data(env, sid)
30
+ handle_error(env) do
31
+ get_session_with_lock(env, sid)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Get session with implemented locking strategy.
38
+ def get_session_with_lock(env, sid)
39
+ expires_at = nil
40
+ result = nil
41
+ max_attempt_date = Time.now.to_f + @config.lock_max_wait_time
42
+ while result.nil?
43
+ exceeded_wait_time?(max_attempt_date)
44
+ begin
45
+ result = attempt_set_lock(sid)
46
+ rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException
47
+ expires_at ||= get_expire_date(sid)
48
+ next if expires_at.nil?
49
+ result = bust_lock(sid, expires_at)
50
+ wait_to_retry(result)
51
+ end
52
+ end
53
+ get_data(env, result)
54
+ end
55
+
56
+ # Determine if session has waited too long to obtain lock.
57
+ #
58
+ # @raise [Error] When time for attempting to get lock has
59
+ # been exceeded.
60
+ def exceeded_wait_time?(max_attempt_date)
61
+ lock_error = AWS::SessionStore::DynamoDB::LockWaitTimeoutError
62
+ raise lock_error if Time.now.to_f > max_attempt_date
63
+ end
64
+
65
+ # @return [Hash] Options hash for placing a lock on a session.
66
+ def get_lock_time_opts(sid)
67
+ merge_all(table_opts(sid), lock_opts)
68
+ end
69
+
70
+ # @return [Time] Time stamp for which the session was locked.
71
+ def lock_time(sid)
72
+ result = @config.dynamo_db_client.get_item(get_lock_time_opts(sid))
73
+ (result[:item]["locked_at"][:n]).to_f if result[:item]["locked_at"]
74
+ end
75
+
76
+ # @return [String] Session data.
77
+ def get_data(env, result)
78
+ lock_time = result[:attributes]["locked_at"][:n]
79
+ env["locked_at"] = (lock_time).to_f
80
+ env['rack.initial_data'] = result[:item]["data"][:s] if result[:item]
81
+ unpack_data(result[:attributes]["data"][:s])
82
+ end
83
+
84
+ # Attempt to bust the lock if the expiration date has expired.
85
+ def bust_lock(sid, expires_at)
86
+ if expires_at < Time.now.to_f
87
+ @config.dynamo_db_client.update_item(obtain_lock_opts(sid))
88
+ end
89
+ end
90
+
91
+ # @return [Hash] Options hash for obtaining the lock.
92
+ def obtain_lock_opts(sid, add_opt = {})
93
+ merge_all(table_opts(sid), lock_attr, add_opt)
94
+ end
95
+
96
+ # Sleep for given time period if the session is currently locked.
97
+ def wait_to_retry(result)
98
+ sleep(0.001 * @config.lock_retry_delay) if result.nil?
99
+ end
100
+
101
+ # Get the expiration date for the session
102
+ def get_expire_date(sid)
103
+ lock_date = lock_time(sid)
104
+ lock_date + (0.001 * @config.lock_expiry_time) if lock_date
105
+ end
106
+
107
+ # Attempt to place a lock on the session.
108
+ def attempt_set_lock(sid)
109
+ @config.dynamo_db_client.update_item(obtain_lock_opts(sid, lock_expect))
110
+ end
111
+
112
+ # Lock attribute - time stamp of when session was locked.
113
+ def lock_attr
114
+ {
115
+ :attribute_updates => {"locked_at" => updated_at},
116
+ :return_values => "ALL_NEW"
117
+ }
118
+ end
119
+
120
+ # Time in which session was updated.
121
+ def updated_at
122
+ { :value => {:n => "#{(Time.now).to_f}"}, :action => "PUT" }
123
+ end
124
+
125
+ # Attributes for locking.
126
+ def add_lock_attrs(env)
127
+ {
128
+ :add_attrs => add_attr, :expect_attr => expect_lock_time(env)
129
+ }
130
+ end
131
+
132
+ # Lock options for setting lock.
133
+ def set_lock_options(env, options = {})
134
+ merge_all(options, add_lock_attrs(env))
135
+ end
136
+
137
+ # Lock expectation.
138
+ def lock_expect
139
+ { :expected => { "locked_at" => { :exists => false } } }
140
+ end
141
+
142
+ # Option to delete lock.
143
+ def add_attr
144
+ { "locked_at" => {:action => "DELETE"} }
145
+ end
146
+
147
+ # Expectation of when lock was set.
148
+ def expect_lock_time(env)
149
+ { :expected => {"locked_at" => {
150
+ :value => {:n => "#{env["locked_at"]}"}, :exists => true}} }
151
+ end
152
+
153
+ # Attributes to be retrieved via client
154
+ def lock_opts
155
+ {:attributes_to_get => ["locked_at"],
156
+ :consistent_read => @config.consistent_read}
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,21 @@
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
+ module AWS::SessionStore::DynamoDB
16
+ class MissingSecretKeyError < RuntimeError
17
+ def initialize(msg = "No secret key provided!")
18
+ super
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,130 @@
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 'rack/session/abstract/id'
15
+ require 'openssl'
16
+ require 'aws-sdk'
17
+
18
+ module AWS::SessionStore::DynamoDB
19
+ # This class is an ID based Session Store Rack Middleware
20
+ # that uses a DynamoDB backend for session storage.
21
+ class RackMiddleware < Rack::Session::Abstract::ID
22
+
23
+ # Initializes SessionStore middleware.
24
+ #
25
+ # @param app Rack application.
26
+ # @option (see Configuration#initialize)
27
+ # @raise [AWS::DynamoDB::Errors::ResourceNotFoundException] If valid table
28
+ # name is not provided.
29
+ # @raise [AWS::SessionStore::DynamoDB::MissingSecretKey] If secret key is
30
+ # not provided.
31
+ def initialize(app, options = {})
32
+ super
33
+ @config = Configuration.new(options)
34
+ set_locking_strategy
35
+ end
36
+
37
+ private
38
+
39
+ # Sets locking strategy for session handler
40
+ #
41
+ # @return [Locking::Null] If locking is not enabled.
42
+ # @return [Locking::Pessimistic] If locking is enabled.
43
+ def set_locking_strategy
44
+ if @config.enable_locking
45
+ @lock = AWS::SessionStore::DynamoDB::Locking::Pessimistic.new(@config)
46
+ else
47
+ @lock = AWS::SessionStore::DynamoDB::Locking::Null.new(@config)
48
+ end
49
+ end
50
+
51
+ # Determines if the correct session table name is being used for
52
+ # this application. Also tests existence of secret key.
53
+ #
54
+ # @raise [AWS::DynamoDB::Errors::ResourceNotFoundException] If wrong table
55
+ # name.
56
+ def validate_config
57
+ raise MissingSecretKeyError unless @config.secret_key
58
+ end
59
+
60
+ # Gets session data.
61
+ def get_session(env, sid)
62
+ validate_config
63
+ case verify_hmac(sid)
64
+ when nil
65
+ set_new_session_properties(env)
66
+ when false
67
+ handle_error {raise InvalidIDError}
68
+ set_new_session_properties(env)
69
+ else
70
+ data = @lock.get_session_data(env, sid)
71
+ [sid, data || {}]
72
+ end
73
+ end
74
+
75
+ def set_new_session_properties(env)
76
+ env['dynamo_db.new_session'] = 'true'
77
+ [generate_sid, {}]
78
+ end
79
+
80
+ # Sets the session in the database after packing data.
81
+ #
82
+ # @return [Hash] If session has been saved.
83
+ # @return [false] If session has could not be saved.
84
+ def set_session(env, sid, session, options)
85
+ @lock.set_session_data(env, sid, session, options)
86
+ end
87
+
88
+ # Destroys session and removes session from database.
89
+ #
90
+ # @return [String] return a new session id or nil if options[:drop]
91
+ def destroy_session(env, sid, options)
92
+ @lock.delete_session(env, sid)
93
+ generate_sid unless options[:drop]
94
+ end
95
+
96
+ # Each database operation is placed in this rescue wrapper.
97
+ # This wrapper will call the method, rescue any exceptions and then pass
98
+ # exceptions to the configured session handler.
99
+ def handle_error(env = nil, &block)
100
+ begin
101
+ yield
102
+ rescue AWS::DynamoDB::Errors::Base,
103
+ AWS::SessionStore::DynamoDB::InvalidIDError => e
104
+ @config.error_handler.handle_error(e, env)
105
+ end
106
+ end
107
+
108
+ # Generate HMAC hash based on MD5
109
+ def generate_hmac(sid, secret)
110
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::MD5.new, secret, sid).strip()
111
+ end
112
+
113
+ # Generate sid with HMAC hash
114
+ def generate_sid(secure = @sid_secure)
115
+ sid = super(secure)
116
+ sid = "#{generate_hmac(sid, @config.secret_key)}--" + sid
117
+ end
118
+
119
+ # Verify digest of HMACed hash
120
+ #
121
+ # @return [true] If the HMAC id has been verified.
122
+ # @return [false] If the HMAC id has been corrupted.
123
+ def verify_hmac(sid)
124
+ return unless sid
125
+ digest, ver_sid = sid.split("--")
126
+ return false unless ver_sid
127
+ digest == generate_hmac(ver_sid, @config.secret_key)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,28 @@
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
+ module AWS::SessionStore::DynamoDB
16
+ class Railtie < Rails::Railtie
17
+ initializer 'aws-sessionstore-dynamodb-rack-middleware' do
18
+ ActionDispatch::Session::DynamodbStore = AWS::SessionStore::DynamoDB::RackMiddleware
19
+ end
20
+
21
+ # Load all rake tasks
22
+ rake_tasks do
23
+ Dir[File.expand_path("../tasks/*.rake", __FILE__)].each do |rake_task|
24
+ load rake_task
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,98 @@
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 'aws-sdk'
15
+ require 'logger'
16
+
17
+ module AWS::SessionStore::DynamoDB
18
+ # This class provides a way to create and delete a session table.
19
+ module Table
20
+ module_function
21
+
22
+ # Creates a session table.
23
+ # @option (see Configuration#initialize)
24
+ def create_table(options = {})
25
+ config = load_config(options)
26
+ ddb_options = properties(config.table_name, config.table_key).merge(
27
+ throughput(config.read_capacity, config.write_capacity)
28
+ )
29
+ config.dynamo_db_client.create_table(ddb_options)
30
+ logger << "Table #{config.table_name} created, waiting for activation...\n"
31
+ block_until_created(config)
32
+ logger << "Table #{config.table_name} is now ready to use.\n"
33
+ rescue AWS::DynamoDB::Errors::ResourceInUseException
34
+ logger << "Table #{config.table_name} already exists, skipping creation.\n"
35
+ end
36
+
37
+ # Deletes a session table.
38
+ # @option (see Configuration#initialize)
39
+ def delete_table(options = {})
40
+ config = load_config(options)
41
+ config.dynamo_db_client.delete_table(:table_name => config.table_name)
42
+ end
43
+
44
+ # @api private
45
+ def logger
46
+ @logger ||= Logger.new($STDOUT)
47
+ end
48
+
49
+ # Loads configuration options.
50
+ # @option (see Configuration#initialize)
51
+ # @api private
52
+ def load_config(options = {})
53
+ AWS::SessionStore::DynamoDB::Configuration.new(options)
54
+ end
55
+
56
+ # @return [Hash] Attribute settings for creating a session table.
57
+ # @api private
58
+ def attributes(hash_key)
59
+ attributes = [{:attribute_name => hash_key, :attribute_type => 'S'}]
60
+ { :attribute_definitions => attributes }
61
+ end
62
+
63
+ # @return Shema values for session table
64
+ # @api private
65
+ def schema(table_name, hash_key)
66
+ {
67
+ :table_name => table_name,
68
+ :key_schema => [ {:attribute_name => hash_key, :key_type => 'HASH'} ]
69
+ }
70
+ end
71
+
72
+ # @return Throughput for Session table
73
+ # @api private
74
+ def throughput(read, write)
75
+ units = {:read_capacity_units=> read, :write_capacity_units => write}
76
+ { :provisioned_throughput => units }
77
+ end
78
+
79
+ # @return Properties for Session table
80
+ # @api private
81
+ def properties(table_name, hash_key)
82
+ attributes(hash_key).merge(schema(table_name, hash_key))
83
+ end
84
+
85
+ # @api private
86
+ def block_until_created(config)
87
+ created = false
88
+ until created
89
+ params = { :table_name => config.table_name }
90
+ response = config.dynamo_db_client.describe_table(params)
91
+ created = response[:table][:table_status] == 'ACTIVE'
92
+
93
+ sleep 10
94
+ end
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,21 @@
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
+ namespace "db" do
15
+ namespace "sessions" do
16
+ desc 'Clean up old sessions in Amazon DynamoDB session store'
17
+ task :cleanup => :environment do |t|
18
+ AWS::SessionStore::DynamoDB::GarbageCollection.collect_garbage
19
+ end
20
+ end
21
+ end