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