global_session 1.1.0 → 2.0.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.
data/README.rdoc CHANGED
@@ -52,15 +52,6 @@ particular, it does not provide any of the following:
52
52
  ...
53
53
  @current_user = User.find(global_session['user'])
54
54
 
55
- 5) For easier programming, enable seamless integration with the local session:
56
- (in global_session.yml)
57
- common:
58
- integrated: true
59
-
60
- (in your controllers)
61
- session['user'] = @user.id #goes to the global session
62
- session['local_thingie'] = @thingie.id #goes to the local session
63
-
64
55
  = Global Session Contents
65
56
 
66
57
  Global session state is stored as a cookie in the user's browser. The cookie
@@ -176,7 +167,6 @@ is a good choice). In the GlobalSession configuration file, specify the
176
167
  class name of the directory under the 'common' section, like so:
177
168
 
178
169
  common:
179
- integrated: true
180
170
  directory: MyCoolDirectory
181
171
 
182
172
  Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>, released under the MIT license
@@ -7,8 +7,8 @@ spec = Gem::Specification.new do |s|
7
7
  s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
8
8
 
9
9
  s.name = 'global_session'
10
- s.version = '1.1.0'
11
- s.date = '2012-11-01'
10
+ s.version = '2.0.0'
11
+ s.date = '2012-11-06'
12
12
 
13
13
  s.authors = ['Tony Spataro']
14
14
  s.email = 'support@rightscale.com'
@@ -21,6 +21,7 @@ spec = Gem::Specification.new do |s|
21
21
 
22
22
  s.add_runtime_dependency('simple_uuid', [">= 0.2.0"])
23
23
  s.add_runtime_dependency('json', ["~> 1.4"])
24
+ s.add_runtime_dependency('msgpack', ["~> 0.4"])
24
25
  s.add_runtime_dependency('rack-contrib', ["~> 1.0"])
25
26
 
26
27
  basedir = File.dirname(__FILE__)
@@ -32,7 +32,6 @@ module GlobalSession
32
32
  # * attributes
33
33
  # * signed
34
34
  # * insecure
35
- # * integrated
36
35
  # * ephemeral
37
36
  # * timeout
38
37
  # * renew
@@ -78,6 +77,10 @@ module GlobalSession
78
77
  "<#{self.class.name} @environment=#{@environment.inspect}>"
79
78
  end
80
79
 
80
+ def to_hash
81
+ @config.dup
82
+ end
83
+
81
84
  # Create a new Configuration object
82
85
  #
83
86
  # === Parameters
@@ -118,7 +121,7 @@ module GlobalSession
118
121
  end
119
122
 
120
123
  def validate # :nodoc
121
- ['attributes/signed', 'integrated', 'cookie/name',
124
+ ['attributes/signed', 'cookie/name',
122
125
  'timeout'].each {|k| validate_presence_of k}
123
126
  end
124
127
 
@@ -95,7 +95,6 @@ module GlobalSession
95
95
  # === Parameters
96
96
  # directory(Directory):: directory implementation that the session should use for various operations
97
97
  # cookie(String):: Optional, serialized global session cookie. If none is supplied, a new session is created.
98
- # valid_signature_digest(String):: Optional, already-trusted signature. If supplied, the expensive RSA-verify operation will be skipped if the cookie's signature matches the value supplied.
99
98
  #
100
99
  # === Return
101
100
  # session(Session):: the newly-initialized session
@@ -26,7 +26,7 @@ module GlobalSession
26
26
  # JSON serializer, used to serialize Hash objects in a form suitable
27
27
  # for stuffing into a cookie.
28
28
  #
29
- class JSON
29
+ module JSON
30
30
  # Unserialize JSON to Hash.
31
31
  #
32
32
  # === Parameters
@@ -50,6 +50,18 @@ module GlobalSession
50
50
  end
51
51
  end
52
52
 
53
+ # Wrapper module for MessagePack that makes it conform to the standard load/dump interface
54
+ # for serializers.
55
+ module Msgpack
56
+ def self.load(binary)
57
+ MessagePack.unpack(binary)
58
+ end
59
+
60
+ def self.dump(object)
61
+ object.to_msgpack
62
+ end
63
+ end
64
+
53
65
  # Implements URL encoding, but without newlines, and using '-' and '_' as
54
66
  # the 62nd and 63rd symbol instead of '+' and '/'. This makes for encoded
55
67
  # values that can be easily stored in a cookie; however, they cannot
@@ -69,7 +69,7 @@ module GlobalSession
69
69
  namespace = namespace.const_get(parts.shift.to_sym) until parts.empty?
70
70
  directory_klass = namespace
71
71
  rescue Exception => e
72
- raise ConfigurationError, "Invalid/unknown directory class name #{@configuration['directory']}"
72
+ raise GlobalSession::ConfigurationError, "Invalid/unknown directory class name #{@configuration['directory']}"
73
73
  end
74
74
 
75
75
  if directory.instance_of?(String)
@@ -37,9 +37,6 @@ module GlobalSession
37
37
  module ActionControllerInstanceMethods
38
38
  def self.included(base) # :nodoc:
39
39
  #Make sure a superclass hasn't already chained the methods...
40
- unless base.instance_methods.include?("session_without_global_session")
41
- base.alias_method_chain :session, :global_session
42
- end
43
40
  unless base.instance_methods.include?("log_processing_without_global_session")
44
41
  base.alias_method_chain :log_processing, :global_session
45
42
  end
@@ -67,26 +64,6 @@ module GlobalSession
67
64
  @global_session
68
65
  end
69
66
 
70
- # Aliased version of ActionController::Base#session which will return the integrated
71
- # global-and-local session object (IntegratedSession).
72
- #
73
- # === Return
74
- # session(IntegratedSession):: the integrated session
75
- def session_with_global_session
76
- if global_session_options[:integrated] && global_session
77
- unless @integrated_session &&
78
- (@integrated_session.local == session_without_global_session) &&
79
- (@integrated_session.global == global_session)
80
- @integrated_session =
81
- IntegratedSession.new(session_without_global_session, global_session)
82
- end
83
-
84
- return @integrated_session
85
- else
86
- return session_without_global_session
87
- end
88
- end
89
-
90
67
  # Filter to initialize the global session.
91
68
  #
92
69
  # === Return
@@ -19,8 +19,6 @@
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
- basedir = File.dirname(__FILE__)
23
-
24
22
  require 'rack/contrib/cookies'
25
23
  require 'action_pack'
26
24
  require 'action_controller'
@@ -0,0 +1,83 @@
1
+ module GlobalSession::Session
2
+ # An abstract base class for all versions of the global session.
3
+ # Defines common attributes and methods.
4
+ class Abstract
5
+ attr_reader :id, :authority, :created_at, :expired_at, :directory
6
+ attr_reader :signed, :insecure
7
+
8
+ # Create a new global session object.
9
+ #
10
+ # === Parameters
11
+ # directory(Directory):: directory implementation that the session should use for various operations
12
+ #
13
+ # ===Raise
14
+ # InvalidSession:: if the session contained in the cookie has been invalidated
15
+ # ExpiredSession:: if the session contained in the cookie has expired
16
+ # MalformedCookie:: if the cookie was corrupt or malformed
17
+ # SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
18
+ def initialize(directory)
19
+ @directory = directory
20
+ @signed = {}
21
+ @insecure = {}
22
+ end
23
+
24
+ # @return a representation of the object suitable for printing to the console
25
+ def inspect
26
+ "<#{self.class.name}(#{self.id})>"
27
+ end
28
+
29
+ # @return a Hash representation of the session with three subkeys: :metadata, :signed and :insecure
30
+ # @raise nothing -- does not raise; returns empty hash if there is a failure
31
+ def to_hash
32
+ hash = {}
33
+
34
+ md = {}
35
+ signed = {}
36
+ insecure = {}
37
+
38
+ hash[:metadata] = md
39
+ hash[:signed] = signed
40
+ hash[:insecure] = insecure
41
+
42
+ md[:id] = @id
43
+ md[:authority] = @authority
44
+ md[:created_at] = @created_at
45
+ md[:expired_at] = @expired_at
46
+ @signed.each_pair { |k, v| signed[k] = v }
47
+ @insecure.each_pair { |k, v| insecure[k] = v }
48
+
49
+ hash
50
+ rescue Exception => e
51
+ {}
52
+ end
53
+
54
+ # Invalidate this session by reporting its UUID to the Directory.
55
+ #
56
+ # === Return
57
+ # unknown(Object):: Returns whatever the Directory returns
58
+ def invalidate!
59
+ @directory.report_invalid_session(@id, @expired_at)
60
+ end
61
+
62
+ # Renews this global session, changing its expiry timestamp into the future.
63
+ # Causes a new signature will be computed when the session is next serialized.
64
+ #
65
+ # === Return
66
+ # true:: Always returns true
67
+ def renew!(expired_at=nil)
68
+ authority_check
69
+ minutes = Integer(@configuration['timeout'])
70
+ expired_at ||= Time.at(Time.now.utc + 60 * minutes)
71
+ @expired_at = expired_at
72
+ @created_at = Time.now.utc
73
+ end
74
+
75
+ private
76
+
77
+ def authority_check # :nodoc:
78
+ unless @directory.local_authority_name
79
+ raise GlobalSession::NoAuthority, 'Cannot change secure session attributes; we are not an authority'
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,352 @@
1
+ # Copyright (c) 2012 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ # Standard library dependencies
23
+ require 'set'
24
+ require 'zlib'
25
+
26
+ module GlobalSession::Session
27
+ # Ladies and gentlemen: the one and only, star of the show, GLOBAL SESSION!
28
+ #
29
+ # Session is designed to act as much like a Hash as possible. You can use
30
+ # most of the methods you would use with Hash: [], has_key?, each, etc. It has a
31
+ # few additional methods that are specific to itself, mostly involving whether
32
+ # it's expired, valid, supports a certain key, etc.
33
+ #
34
+ class V1 < Abstract
35
+ # Utility method to decode a cookie; good for console debugging. This performs no
36
+ # validation or security check of any sort.
37
+ #
38
+ # === Parameters
39
+ # cookie(String):: well-formed global session cookie
40
+ def self.decode_cookie(cookie)
41
+ zbin = GlobalSession::Encoding::Base64Cookie.load(cookie)
42
+ json = Zlib::Inflate.inflate(zbin)
43
+ return GlobalSession::Encoding::JSON.load(json)
44
+ end
45
+
46
+ # Create a new global session object.
47
+ #
48
+ # === Parameters
49
+ # directory(Directory):: directory implementation that the session should use for various operations
50
+ # cookie(String):: Optional, serialized global session cookie. If none is supplied, a new session is created.
51
+ # valid_signature_digest(String):: Optional, already-trusted signature. If supplied, the expensive RSA-verify operation will be skipped if the cookie's signature matches the value supplied.
52
+ #
53
+ # ===Raise
54
+ # InvalidSession:: if the session contained in the cookie has been invalidated
55
+ # ExpiredSession:: if the session contained in the cookie has expired
56
+ # MalformedCookie:: if the cookie was corrupt or malformed
57
+ # SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
58
+ def initialize(directory, cookie=nil, valid_signature_digest=nil)
59
+ super(directory)
60
+ @configuration = directory.configuration
61
+ @schema_signed = Set.new((@configuration['attributes']['signed']))
62
+ @schema_insecure = Set.new((@configuration['attributes']['insecure']))
63
+
64
+ if cookie && !cookie.empty?
65
+ load_from_cookie(cookie, valid_signature_digest)
66
+ elsif @directory.local_authority_name
67
+ create_from_scratch
68
+ else
69
+ create_invalid
70
+ end
71
+ end
72
+
73
+ # @return [true,false] true if this session was created in-process, false if it was initialized from a cookie
74
+ def new_record?
75
+ @cookie.nil?
76
+ end
77
+
78
+ # Determine whether the session is valid. This method simply delegates to the
79
+ # directory associated with this session.
80
+ #
81
+ # === Return
82
+ # valid(true|false):: True if the session is valid, false otherwise
83
+ def valid?
84
+ @directory.valid_session?(@id, @expired_at)
85
+ end
86
+
87
+ # Serialize the session to a form suitable for use with HTTP cookies. If any
88
+ # secure attributes have changed since the session was instantiated, compute
89
+ # a fresh RSA signature.
90
+ #
91
+ # === Return
92
+ # cookie(String):: The B64cookie-encoded Zlib-compressed JSON-serialized global session hash
93
+ def to_s
94
+ if @cookie && !@dirty_insecure && !@dirty_secure
95
+ #use cached cookie if nothing has changed
96
+ return @cookie
97
+ end
98
+
99
+ hash = {'id' => @id,
100
+ 'tc' => @created_at.to_i, 'te' => @expired_at.to_i,
101
+ 'ds' => @signed}
102
+
103
+ if @signature && !@dirty_secure
104
+ #use cached signature unless we've changed secure state
105
+ authority = @authority
106
+ else
107
+ authority_check
108
+ authority = @directory.local_authority_name
109
+ hash['a'] = authority
110
+ digest = canonical_digest(hash)
111
+ @signature = GlobalSession::Encoding::Base64Cookie.dump(@directory.private_key.private_encrypt(digest))
112
+ end
113
+
114
+ hash['dx'] = @insecure
115
+ hash['s'] = @signature
116
+ hash['a'] = authority
117
+
118
+ json = GlobalSession::Encoding::JSON.dump(hash)
119
+ zbin = Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
120
+ return GlobalSession::Encoding::Base64Cookie.dump(zbin)
121
+ end
122
+
123
+ # Determine whether the global session schema allows a given key to be placed
124
+ # in the global session.
125
+ #
126
+ # === Parameters
127
+ # key(String):: The name of the key
128
+ #
129
+ # === Return
130
+ # supported(true|false):: Whether the specified key is supported
131
+ def supports_key?(key)
132
+ @schema_signed.include?(key) || @schema_insecure.include?(key)
133
+ end
134
+
135
+ # Determine whether this session contains a value with the specified key.
136
+ #
137
+ # === Parameters
138
+ # key(String):: The name of the key
139
+ #
140
+ # === Return
141
+ # contained(true|false):: Whether the session currently has a value for the specified key.
142
+ def has_key?(key)
143
+ @signed.has_key?(key) || @insecure.has_key?(key)
144
+ end
145
+
146
+ alias :key? :has_key?
147
+
148
+ # Return the keys that are currently present in the global session.
149
+ #
150
+ # === Return
151
+ # keys(Array):: List of keys contained in the global session
152
+ def keys
153
+ @signed.keys + @insecure.keys
154
+ end
155
+
156
+ # Return the values that are currently present in the global session.
157
+ #
158
+ # === Return
159
+ # values(Array):: List of values contained in the global session
160
+ def values
161
+ @signed.values + @insecure.values
162
+ end
163
+
164
+ # Iterate over each key/value pair
165
+ #
166
+ # === Block
167
+ # An iterator which will be called with each key/value pair
168
+ #
169
+ # === Return
170
+ # Returns the value of the last expression evaluated by the block
171
+ def each_pair(&block) # :yields: |key, value|
172
+ @signed.each_pair(&block)
173
+ @insecure.each_pair(&block)
174
+ end
175
+
176
+ # Lookup a value by its key.
177
+ #
178
+ # === Parameters
179
+ # key(String):: the key
180
+ #
181
+ # === Return
182
+ # value(Object):: The value associated with +key+, or nil if +key+ is not present
183
+ def [](key)
184
+ key = key.to_s #take care of symbol-style keys
185
+ @signed[key] || @insecure[key]
186
+ end
187
+
188
+ # Set a value in the global session hash. If the supplied key is denoted as
189
+ # secure by the global session schema, causes a new signature to be computed
190
+ # when the session is next serialized.
191
+ #
192
+ # === Parameters
193
+ # key(String):: The key to set
194
+ # value(Object):: The value to set
195
+ #
196
+ # === Return
197
+ # value(Object):: Always returns the value that was set
198
+ #
199
+ # ===Raise
200
+ # InvalidSession:: if the session has been invalidated (and therefore can't be written to)
201
+ # ArgumentError:: if the configuration doesn't define the specified key as part of the global session
202
+ # NoAuthority:: if the specified key is secure and the local node is not an authority
203
+ # UnserializableType:: if the specified value can't be serialized as JSON
204
+ def []=(key, value)
205
+ key = key.to_s #take care of symbol-style keys
206
+ raise GlobalSession::InvalidSession unless valid?
207
+
208
+ #Ensure that the value is serializable (will raise if not)
209
+ canonicalize(value)
210
+
211
+ if @schema_signed.include?(key)
212
+ authority_check
213
+ @signed[key] = value
214
+ @dirty_secure = true
215
+ elsif @schema_insecure.include?(key)
216
+ @insecure[key] = value
217
+ @dirty_insecure = true
218
+ else
219
+ raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
220
+ end
221
+
222
+ return value
223
+ end
224
+
225
+ # Renews this global session, changing its expiry timestamp into the future.
226
+ # Causes a new signature will be computed when the session is next serialized.
227
+ #
228
+ # === Return
229
+ # true:: Always returns true
230
+ def renew!(expired_at=nil)
231
+ super(expired_at)
232
+ @dirty_secure = true
233
+ end
234
+
235
+ # Return the SHA1 hash of the most recently-computed RSA signature of this session.
236
+ # This isn't really intended for the end user; it exists so the Web framework integration
237
+ # code can optimize request speed by caching the most recently verified signature in the
238
+ # local session and avoid re-verifying it on every request.
239
+ #
240
+ # === Return
241
+ # digest(String):: SHA1 hex-digest of most-recently-computed signature
242
+ def signature_digest
243
+ @signature ? digest(@signature) : nil
244
+ end
245
+
246
+ private
247
+
248
+ def canonical_digest(input) # :nodoc:
249
+ canonical = GlobalSession::Encoding::JSON.dump(canonicalize(input))
250
+ return digest(canonical)
251
+ end
252
+
253
+ def digest(input) # :nodoc:
254
+ return Digest::SHA1.new().update(input).hexdigest
255
+ end
256
+
257
+ def canonicalize(input) # :nodoc:
258
+ case input
259
+ when Hash
260
+ output = Array.new
261
+ ordered_keys = input.keys.sort
262
+ ordered_keys.each do |key|
263
+ output << [canonicalize(key), canonicalize(input[key])]
264
+ end
265
+ when Array
266
+ output = input.collect { |x| canonicalize(x) }
267
+ when Numeric, String, NilClass
268
+ output = input
269
+ else
270
+ raise UnserializableType, "Objects of type #{input.class.name} cannot be serialized in the global session"
271
+ end
272
+
273
+ return output
274
+ end
275
+
276
+ def load_from_cookie(cookie, valid_signature_digest) # :nodoc:
277
+ begin
278
+ zbin = GlobalSession::Encoding::Base64Cookie.load(cookie)
279
+ json = Zlib::Inflate.inflate(zbin)
280
+ hash = GlobalSession::Encoding::JSON.load(json)
281
+ rescue Exception => e
282
+ mc = GlobalSession::MalformedCookie.new("Caused by #{e.class.name}: #{e.message}")
283
+ mc.set_backtrace(e.backtrace)
284
+ raise mc
285
+ end
286
+
287
+ id = hash['id']
288
+ authority = hash['a']
289
+ created_at = Time.at(hash['tc'].to_i).utc
290
+ expired_at = Time.at(hash['te'].to_i).utc
291
+ signed = hash['ds']
292
+ insecure = hash.delete('dx')
293
+ signature = hash.delete('s')
294
+
295
+ unless valid_signature_digest == digest(signature)
296
+ #Check signature
297
+ expected = canonical_digest(hash)
298
+ signer = @directory.authorities[authority]
299
+ raise SecurityError, "Unknown signing authority #{authority}" unless signer
300
+ got = signer.public_decrypt(GlobalSession::Encoding::Base64Cookie.load(signature))
301
+ unless (got == expected)
302
+ raise SecurityError, "Signature mismatch on global session cookie; tampering suspected"
303
+ end
304
+ end
305
+
306
+ #Check trust in signing authority
307
+ unless @directory.trusted_authority?(authority)
308
+ raise SecurityError, "Global sessions signed by #{authority} are not trusted"
309
+ end
310
+
311
+ #Check expiration
312
+ unless expired_at > Time.now.utc
313
+ raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
314
+ end
315
+
316
+ #Check other validity (delegate to directory)
317
+ unless @directory.valid_session?(id, expired_at)
318
+ raise GlobalSession::InvalidSession, "Global session has been invalidated"
319
+ end
320
+
321
+ #If all validation stuff passed, assign our instance variables.
322
+ @id = id
323
+ @authority = authority
324
+ @created_at = created_at
325
+ @expired_at = expired_at
326
+ @signed = signed
327
+ @insecure = insecure
328
+ @signature = signature
329
+ @cookie = cookie
330
+ end
331
+
332
+ def create_from_scratch # :nodoc:
333
+ authority_check
334
+
335
+ @signed = {}
336
+ @insecure = {}
337
+ @created_at = Time.now.utc
338
+ @authority = @directory.local_authority_name
339
+ @id = RightSupport::Data::UUID.generate
340
+ renew!
341
+ end
342
+
343
+ def create_invalid # :nodoc:
344
+ @id = nil
345
+ @created_at = Time.now.utc
346
+ @expired_at = created_at
347
+ @signed = {}
348
+ @insecure = {}
349
+ @authority = nil
350
+ end
351
+ end
352
+ end