global_session 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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