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.
@@ -0,0 +1,357 @@
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
+
25
+ # Dependencies on other gems
26
+ require 'msgpack'
27
+
28
+ module GlobalSession::Session
29
+ class V2 < Abstract
30
+ # Utility method to decode a cookie; good for console debugging. This performs no
31
+ # validation or security check of any sort.
32
+ #
33
+ # === Parameters
34
+ # cookie(String):: well-formed global session cookie
35
+ def self.decode_cookie(cookie)
36
+ msgpack = GlobalSession::Encoding::Base64Cookie.load(cookie)
37
+ return GlobalSession::Encoding::Msgpack.load(msgpack)
38
+ end
39
+
40
+ # Create a new global session object.
41
+ #
42
+ # === Parameters
43
+ # directory(Directory):: directory implementation that the session should use for various operations
44
+ # cookie(String):: Optional, serialized global session cookie. If none is supplied, a new session is created.
45
+ # unused(Object):: Optional, already-trusted signature. This is ignored for v2.
46
+ #
47
+ # ===Raise
48
+ # InvalidSession:: if the session contained in the cookie has been invalidated
49
+ # ExpiredSession:: if the session contained in the cookie has expired
50
+ # MalformedCookie:: if the cookie was corrupt or malformed
51
+ # SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
52
+ def initialize(directory, cookie=nil, unused=nil)
53
+ super(directory)
54
+ @configuration = directory.configuration
55
+ @schema_signed = Set.new((@configuration['attributes']['signed']))
56
+ @schema_insecure = Set.new((@configuration['attributes']['insecure']))
57
+
58
+ if cookie && !cookie.empty?
59
+ load_from_cookie(cookie)
60
+ elsif @directory.local_authority_name
61
+ create_from_scratch
62
+ else
63
+ create_invalid
64
+ end
65
+ end
66
+
67
+ # @return [true,false] true if this session was created in-process, false if it was initialized from a cookie
68
+ def new_record?
69
+ @cookie.nil?
70
+ end
71
+
72
+ # Determine whether the session is valid. This method simply delegates to the
73
+ # directory associated with this session.
74
+ #
75
+ # === Return
76
+ # valid(true|false):: True if the session is valid, false otherwise
77
+ def valid?
78
+ @directory.valid_session?(@id, @expired_at)
79
+ end
80
+
81
+ # Serialize the session to a form suitable for use with HTTP cookies. If any
82
+ # secure attributes have changed since the session was instantiated, compute
83
+ # a fresh RSA signature.
84
+ #
85
+ # === Return
86
+ # cookie(String):: The B64cookie-encoded Zlib-compressed Msgpack-serialized global session hash
87
+ def to_s
88
+ if @cookie && !@dirty_insecure && !@dirty_secure
89
+ #use cached cookie if nothing has changed
90
+ return @cookie
91
+ end
92
+
93
+ hash = {'id' => @id,
94
+ 'tc' => @created_at.to_i, 'te' => @expired_at.to_i,
95
+ 'ds' => @signed}
96
+
97
+ if @signature && !@dirty_secure
98
+ #use cached signature unless we've changed secure state
99
+ authority = @authority
100
+ else
101
+ authority_check
102
+ authority = @directory.local_authority_name
103
+ hash['a'] = authority
104
+ signed_hash = RightSupport::Crypto::SignedHash.new(
105
+ hash.reject { |k,v| ['dx', 's'].include?(k) },
106
+ :encoding=>GlobalSession::Encoding::Msgpack,
107
+ :private_key=>@directory.private_key)
108
+ @signature = signed_hash.sign(@expired_at)
109
+ end
110
+
111
+ hash['dx'] = @insecure
112
+ hash['s'] = @signature
113
+ hash['a'] = authority
114
+
115
+ array = attribute_hash_to_array(hash)
116
+ msgpack = GlobalSession::Encoding::Msgpack.dump(array)
117
+ return GlobalSession::Encoding::Base64Cookie.dump(msgpack)
118
+ end
119
+
120
+ # Determine whether the global session schema allows a given key to be placed
121
+ # in the global session.
122
+ #
123
+ # === Parameters
124
+ # key(String):: The name of the key
125
+ #
126
+ # === Return
127
+ # supported(true|false):: Whether the specified key is supported
128
+ def supports_key?(key)
129
+ @schema_signed.include?(key) || @schema_insecure.include?(key)
130
+ end
131
+
132
+ # Determine whether this session contains a value with the specified key.
133
+ #
134
+ # === Parameters
135
+ # key(String):: The name of the key
136
+ #
137
+ # === Return
138
+ # contained(true|false):: Whether the session currently has a value for the specified key.
139
+ def has_key?(key)
140
+ @signed.has_key?(key) || @insecure.has_key?(key)
141
+ end
142
+
143
+ alias :key? :has_key?
144
+
145
+ # Return the keys that are currently present in the global session.
146
+ #
147
+ # === Return
148
+ # keys(Array):: List of keys contained in the global session
149
+ def keys
150
+ @signed.keys + @insecure.keys
151
+ end
152
+
153
+ # Return the values that are currently present in the global session.
154
+ #
155
+ # === Return
156
+ # values(Array):: List of values contained in the global session
157
+ def values
158
+ @signed.values + @insecure.values
159
+ end
160
+
161
+ # Iterate over each key/value pair
162
+ #
163
+ # === Block
164
+ # An iterator which will be called with each key/value pair
165
+ #
166
+ # === Return
167
+ # Returns the value of the last expression evaluated by the block
168
+ def each_pair(&block) # :yields: |key, value|
169
+ @signed.each_pair(&block)
170
+ @insecure.each_pair(&block)
171
+ end
172
+
173
+ # Lookup a value by its key.
174
+ #
175
+ # === Parameters
176
+ # key(String):: the key
177
+ #
178
+ # === Return
179
+ # value(Object):: The value associated with +key+, or nil if +key+ is not present
180
+ def [](key)
181
+ key = key.to_s #take care of symbol-style keys
182
+ @signed[key] || @insecure[key]
183
+ end
184
+
185
+ # Set a value in the global session hash. If the supplied key is denoted as
186
+ # secure by the global session schema, causes a new signature to be computed
187
+ # when the session is next serialized.
188
+ #
189
+ # === Parameters
190
+ # key(String):: The key to set
191
+ # value(Object):: The value to set
192
+ #
193
+ # === Return
194
+ # value(Object):: Always returns the value that was set
195
+ #
196
+ # ===Raise
197
+ # InvalidSession:: if the session has been invalidated (and therefore can't be written to)
198
+ # ArgumentError:: if the configuration doesn't define the specified key as part of the global session
199
+ # NoAuthority:: if the specified key is secure and the local node is not an authority
200
+ # UnserializableType:: if the specified value can't be serialized as msgpack
201
+ def []=(key, value)
202
+ key = key.to_s #take care of symbol-style keys
203
+ raise GlobalSession::InvalidSession unless valid?
204
+
205
+ if @schema_signed.include?(key)
206
+ authority_check
207
+ @signed[key] = value
208
+ @dirty_secure = true
209
+ elsif @schema_insecure.include?(key)
210
+ @insecure[key] = value
211
+ @dirty_insecure = true
212
+ else
213
+ raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
214
+ end
215
+
216
+ return value
217
+ end
218
+
219
+ # Renews this global session, changing its expiry timestamp into the future.
220
+ # Causes a new signature will be computed when the session is next serialized.
221
+ #
222
+ # === Return
223
+ # true:: Always returns true
224
+ def renew!(expired_at=nil)
225
+ super(expired_at)
226
+ @dirty_secure = true
227
+ end
228
+
229
+ # Return the SHA1 hash of the most recently-computed RSA signature of this session.
230
+ # This isn't really intended for the end user; it exists so the Web framework integration
231
+ # code can optimize request speed by caching the most recently verified signature in the
232
+ # local session and avoid re-verifying it on every request.
233
+ #
234
+ # === Return
235
+ # digest(String):: SHA1 hex-digest of most-recently-computed signature
236
+ def signature_digest
237
+ @signature ? digest(@signature) : nil
238
+ end
239
+
240
+ private
241
+
242
+ # Transform a V1-style attribute hash to an Array with fixed placement for
243
+ # each element. The V2 scheme stores an array in the cookie instead of a hash
244
+ # to save space.
245
+ #
246
+ # === Parameters
247
+ # hash(Hash):: the attribute hash
248
+ #
249
+ # === Return
250
+ # attributes(Array)::
251
+ #
252
+ def attribute_hash_to_array(hash)
253
+ [
254
+ hash['id'],
255
+ hash['a'],
256
+ hash['tc'],
257
+ hash['te'],
258
+ hash['ds'],
259
+ hash['dx'],
260
+ hash['s']
261
+ ]
262
+ end
263
+
264
+ # Transform a V2-style attribute array to a Hash with the traditional attribute
265
+ # names. This is good for passing to SignedHash, or initializing a V1 session for
266
+ # downrev compatibility.
267
+ #
268
+ # === Parameters
269
+ # hash(Hash):: the attribute hash
270
+ #
271
+ # === Return
272
+ # attributes(Array):: fixed-position attributes array
273
+ #
274
+ def attribute_array_to_hash(array)
275
+ {
276
+ 'id' => array[0],
277
+ 'a' => array[1],
278
+ 'tc' => array[2],
279
+ 'te' => array[3],
280
+ 'ds' => array[4],
281
+ 'dx' => array[5],
282
+ 's' => array[6],
283
+ }
284
+ end
285
+
286
+ def load_from_cookie(cookie) # :nodoc:
287
+ begin
288
+ msgpack = GlobalSession::Encoding::Base64Cookie.load(cookie)
289
+ array = GlobalSession::Encoding::Msgpack.load(msgpack)
290
+ hash = attribute_array_to_hash(array)
291
+ rescue Exception => e
292
+ mc = GlobalSession::MalformedCookie.new("Caused by #{e.class.name}: #{e.message}")
293
+ mc.set_backtrace(e.backtrace)
294
+ raise mc
295
+ end
296
+
297
+ id = hash['id']
298
+ authority = hash['a']
299
+ created_at = Time.at(hash['tc'].to_i).utc
300
+ expired_at = Time.at(hash['te'].to_i).utc
301
+ signed = hash['ds']
302
+ insecure = hash.delete('dx')
303
+ signature = hash.delete('s')
304
+
305
+ #Check trust in signing authority
306
+ unless @directory.trusted_authority?(authority)
307
+ raise SecurityError, "Global sessions signed by #{authority.inspect} are not trusted"
308
+ end
309
+
310
+ signed_hash = RightSupport::Crypto::SignedHash.new(
311
+ hash.reject { |k,v| ['dx', 's'].include?(k) },
312
+ :encoding=>GlobalSession::Encoding::Msgpack,
313
+ :public_key=>@directory.authorities[authority])
314
+ signed_hash.verify!(signature, expired_at)
315
+
316
+ #Check expiration
317
+ unless expired_at > Time.now.utc
318
+ raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
319
+ end
320
+
321
+ #Check other validity (delegate to directory)
322
+ unless @directory.valid_session?(id, expired_at)
323
+ raise GlobalSession::InvalidSession, "Global session has been invalidated"
324
+ end
325
+
326
+ #If all validation stuff passed, assign our instance variables.
327
+ @id = id
328
+ @authority = authority
329
+ @created_at = created_at
330
+ @expired_at = expired_at
331
+ @signed = signed
332
+ @insecure = insecure
333
+ @signature = signature
334
+ @cookie = cookie
335
+ end
336
+
337
+ def create_from_scratch # :nodoc:
338
+ authority_check
339
+
340
+ @signed = {}
341
+ @insecure = {}
342
+ @created_at = Time.now.utc
343
+ @authority = @directory.local_authority_name
344
+ @id = RightSupport::Data::UUID.generate
345
+ renew!
346
+ end
347
+
348
+ def create_invalid # :nodoc:
349
+ @id = nil
350
+ @created_at = Time.now.utc
351
+ @expired_at = created_at
352
+ @signed = {}
353
+ @insecure = {}
354
+ @authority = nil
355
+ end
356
+ end
357
+ end
@@ -1,402 +1,38 @@
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
1
  module GlobalSession
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 Session
35
- attr_reader :id, :authority, :created_at, :expired_at, :directory
36
-
37
- # Utility method to decode a cookie; good for console debugging. This performs no
38
- # validation or security check of any sort.
39
- #
40
- # === Parameters
41
- # cookie(String):: well-formed global session cookie
42
- def self.decode_cookie(cookie)
43
- zbin = Encoding::Base64Cookie.load(cookie)
44
- json = Zlib::Inflate.inflate(zbin)
45
- return Encoding::JSON.load(json)
46
- end
47
-
48
- # @return a representation of the object suitable for printing to the console
49
- def inspect
50
- "<#{self.class.name} @id=#{@id.inspect}>"
51
- end
52
-
53
- # Create a new global session object.
54
- #
55
- # === Parameters
56
- # directory(Directory):: directory implementation that the session should use for various operations
57
- # cookie(String):: Optional, serialized global session cookie. If none is supplied, a new session is created.
58
- # 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.
59
- #
60
- # ===Raise
61
- # InvalidSession:: if the session contained in the cookie has been invalidated
62
- # ExpiredSession:: if the session contained in the cookie has expired
63
- # MalformedCookie:: if the cookie was corrupt or malformed
64
- # SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
65
- def initialize(directory, cookie=nil, valid_signature_digest=nil)
66
- @configuration = directory.configuration
67
- @schema_signed = Set.new((@configuration['attributes']['signed']))
68
- @schema_insecure = Set.new((@configuration['attributes']['insecure']))
69
- @directory = directory
70
-
71
- if cookie && !cookie.empty?
72
- load_from_cookie(cookie, valid_signature_digest)
73
- elsif @directory.local_authority_name
74
- create_from_scratch
75
- else
76
- create_invalid
77
- end
78
- end
79
-
80
- # @return [true,false] true if this session was created in-process, false if it was initialized from a cookie
81
- def new_record?
82
- @cookie.nil?
83
- end
84
-
85
- # @return a Hash representation of the session with three subkeys: :metadata, :signed and :insecure
86
- # @raise nothing -- does not raise; returns empty hash if there is a failure
87
- def to_hash
88
- hash = {}
89
-
90
- md = {}
91
- signed = {}
92
- insecure = {}
93
-
94
- hash[:metadata] = md
95
- hash[:signed] = signed
96
- hash[:insecure] = insecure
97
-
98
- md[:id] = @id
99
- md[:authority] = @authority
100
- md[:created_at] = @created_at
101
- md[:expired_at] = @expired_at
102
- @signed.each_pair { |k, v| signed[k] = v }
103
- @insecure.each_pair { |k, v| insecure[k] = v }
104
-
105
- hash
106
- rescue Exception => e
107
- {}
108
- end
109
-
110
- # Determine whether the session is valid. This method simply delegates to the
111
- # directory associated with this session.
112
- #
113
- # === Return
114
- # valid(true|false):: True if the session is valid, false otherwise
115
- def valid?
116
- @directory.valid_session?(@id, @expired_at)
117
- end
118
-
119
- # Serialize the session to a form suitable for use with HTTP cookies. If any
120
- # secure attributes have changed since the session was instantiated, compute
121
- # a fresh RSA signature.
122
- #
123
- # === Return
124
- # cookie(String):: The B64cookie-encoded Zlib-compressed JSON-serialized global session hash
125
- def to_s
126
- if @cookie && !@dirty_insecure && !@dirty_secure
127
- #use cached cookie if nothing has changed
128
- return @cookie
129
- end
130
-
131
- hash = {'id'=>@id,
132
- 'tc'=>@created_at.to_i, 'te'=>@expired_at.to_i,
133
- 'ds'=>@signed}
134
-
135
- if @signature && !@dirty_secure
136
- #use cached signature unless we've changed secure state
137
- authority = @authority
138
- else
139
- authority_check
140
- authority = @directory.local_authority_name
141
- hash['a'] = authority
142
- digest = canonical_digest(hash)
143
- @signature = Encoding::Base64Cookie.dump(@directory.private_key.private_encrypt(digest))
144
- end
145
-
146
- hash['dx'] = @insecure
147
- hash['s'] = @signature
148
- hash['a'] = authority
149
-
150
- json = Encoding::JSON.dump(hash)
151
- zbin = Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
152
- return Encoding::Base64Cookie.dump(zbin)
153
- end
154
-
155
- # Determine whether the global session schema allows a given key to be placed
156
- # in the global session.
157
- #
158
- # === Parameters
159
- # key(String):: The name of the key
160
- #
161
- # === Return
162
- # supported(true|false):: Whether the specified key is supported
163
- def supports_key?(key)
164
- @schema_signed.include?(key) || @schema_insecure.include?(key)
165
- end
166
-
167
- # Determine whether this session contains a value with the specified key.
168
- #
169
- # === Parameters
170
- # key(String):: The name of the key
171
- #
172
- # === Return
173
- # contained(true|false):: Whether the session currently has a value for the specified key.
174
- def has_key?(key)
175
- @signed.has_key?(key) || @insecure.has_key?(key)
176
- end
177
-
178
- alias :key? :has_key?
179
-
180
- # Return the keys that are currently present in the global session.
181
- #
182
- # === Return
183
- # keys(Array):: List of keys contained in the global session
184
- def keys
185
- @signed.keys + @insecure.keys
186
- end
187
-
188
- # Return the values that are currently present in the global session.
189
- #
190
- # === Return
191
- # values(Array):: List of values contained in the global session
192
- def values
193
- @signed.values + @insecure.values
194
- end
195
-
196
- # Iterate over each key/value pair
197
- #
198
- # === Block
199
- # An iterator which will be called with each key/value pair
200
- #
201
- # === Return
202
- # Returns the value of the last expression evaluated by the block
203
- def each_pair(&block) # :yields: |key, value|
204
- @signed.each_pair(&block)
205
- @insecure.each_pair(&block)
206
- end
207
-
208
- # Lookup a value by its key.
209
- #
210
- # === Parameters
211
- # key(String):: the key
212
- #
213
- # === Return
214
- # value(Object):: The value associated with +key+, or nil if +key+ is not present
215
- def [](key)
216
- key = key.to_s #take care of symbol-style keys
217
- @signed[key] || @insecure[key]
218
- end
219
-
220
- # Set a value in the global session hash. If the supplied key is denoted as
221
- # secure by the global session schema, causes a new signature to be computed
222
- # when the session is next serialized.
223
- #
224
- # === Parameters
225
- # key(String):: The key to set
226
- # value(Object):: The value to set
227
- #
228
- # === Return
229
- # value(Object):: Always returns the value that was set
230
- #
231
- # ===Raise
232
- # InvalidSession:: if the session has been invalidated (and therefore can't be written to)
233
- # ArgumentError:: if the configuration doesn't define the specified key as part of the global session
234
- # NoAuthority:: if the specified key is secure and the local node is not an authority
235
- # UnserializableType:: if the specified value can't be serialized as JSON
236
- def []=(key, value)
237
- key = key.to_s #take care of symbol-style keys
238
- raise InvalidSession unless valid?
239
-
240
- #Ensure that the value is serializable (will raise if not)
241
- canonicalize(value)
242
-
243
- if @schema_signed.include?(key)
244
- authority_check
245
- @signed[key] = value
246
- @dirty_secure = true
247
- elsif @schema_insecure.include?(key)
248
- @insecure[key] = value
249
- @dirty_insecure = true
250
- else
251
- raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
252
- end
253
-
254
- return value
255
- end
256
-
257
- # Invalidate this session by reporting its UUID to the Directory.
258
- #
259
- # === Return
260
- # unknown(Object):: Returns whatever the Directory returns
261
- def invalidate!
262
- @directory.report_invalid_session(@id, @expired_at)
263
- end
264
-
265
- # Renews this global session, changing its expiry timestamp into the future.
266
- # Causes a new signature will be computed when the session is next serialized.
267
- #
268
- # === Return
269
- # true:: Always returns true
270
- def renew!(expired_at=nil)
271
- authority_check
272
- minutes = Integer(@configuration['timeout'])
273
- expired_at ||= Time.at(Time.now.utc + 60 * minutes)
274
- @expired_at = expired_at
275
- @created_at = Time.now.utc
276
- @dirty_secure = true
277
- end
278
-
279
- # Return the SHA1 hash of the most recently-computed RSA signature of this session.
280
- # This isn't really intended for the end user; it exists so the Web framework integration
281
- # code can optimize request speed by caching the most recently verified signature in the
282
- # local session and avoid re-verifying it on every request.
283
- #
284
- # === Return
285
- # digest(String):: SHA1 hex-digest of most-recently-computed signature
286
- def signature_digest
287
- @signature ? digest(@signature) : nil
288
- end
289
-
290
- private
291
-
292
- def authority_check # :nodoc:
293
- unless @directory.local_authority_name
294
- raise NoAuthority, 'Cannot change secure session attributes; we are not an authority'
295
- end
296
- end
297
-
298
- def canonical_digest(input) # :nodoc:
299
- canonical = Encoding::JSON.dump(canonicalize(input))
300
- return digest(canonical)
301
- end
302
-
303
- def digest(input) # :nodoc:
304
- return Digest::SHA1.new().update(input).hexdigest
305
- end
306
-
307
- def canonicalize(input) # :nodoc:
308
- case input
309
- when Hash
310
- output = Array.new
311
- ordered_keys = input.keys.sort
312
- ordered_keys.each do |key|
313
- output << [ canonicalize(key), canonicalize(input[key]) ]
314
- end
315
- when Array
316
- output = input.collect { |x| canonicalize(x) }
317
- when Numeric, String, NilClass
318
- output = input
319
- else
320
- raise UnserializableType, "Objects of type #{input.class.name} cannot be serialized in the global session"
321
- end
322
-
323
- return output
324
- end
325
-
326
- def load_from_cookie(cookie, valid_signature_digest) # :nodoc:
327
- begin
328
- zbin = Encoding::Base64Cookie.load(cookie)
329
- json = Zlib::Inflate.inflate(zbin)
330
- hash = Encoding::JSON.load(json)
331
- rescue Exception => e
332
- mc = MalformedCookie.new("Caused by #{e.class.name}: #{e.message}")
333
- mc.set_backtrace(e.backtrace)
334
- raise mc
335
- end
336
-
337
- id = hash['id']
338
- authority = hash['a']
339
- created_at = Time.at(hash['tc'].to_i).utc
340
- expired_at = Time.at(hash['te'].to_i).utc
341
- signed = hash['ds']
342
- insecure = hash.delete('dx')
343
- signature = hash.delete('s')
344
-
345
- unless valid_signature_digest == digest(signature)
346
- #Check signature
347
- expected = canonical_digest(hash)
348
- signer = @directory.authorities[authority]
349
- raise SecurityError, "Unknown signing authority #{authority}" unless signer
350
- got = signer.public_decrypt(Encoding::Base64Cookie.load(signature))
351
- unless (got == expected)
352
- raise SecurityError, "Signature mismatch on global session cookie; tampering suspected"
353
- end
354
- end
355
-
356
- #Check trust in signing authority
357
- unless @directory.trusted_authority?(authority)
358
- raise SecurityError, "Global sessions signed by #{authority} are not trusted"
359
- end
360
-
361
- #Check expiration
362
- unless expired_at > Time.now.utc
363
- raise ExpiredSession, "Session expired at #{expired_at}"
364
- end
365
-
366
- #Check other validity (delegate to directory)
367
- unless @directory.valid_session?(id, expired_at)
368
- raise InvalidSession, "Global session has been invalidated"
369
- end
370
-
371
- #If all validation stuff passed, assign our instance variables.
372
- @id = id
373
- @authority = authority
374
- @created_at = created_at
375
- @expired_at = expired_at
376
- @signed = signed
377
- @insecure = insecure
378
- @signature = signature
379
- @cookie = cookie
380
- end
381
-
382
- def create_from_scratch # :nodoc:
383
- authority_check
2
+ module Session
3
+ end
4
+ end
384
5
 
385
- @signed = {}
386
- @insecure = {}
387
- @created_at = Time.now.utc
388
- @authority = @directory.local_authority_name
389
- @id = RightSupport::Data::UUID.generate
390
- renew!
391
- end
6
+ require 'global_session/session/abstract'
7
+ require 'global_session/session/v1'
8
+ require 'global_session/session/v2'
392
9
 
393
- def create_invalid # :nodoc:
394
- @id = nil
395
- @created_at = Time.now.utc
396
- @expired_at = created_at
397
- @signed = {}
398
- @insecure = {}
399
- @authority = nil
400
- end
401
- end
10
+ # Ladies and gentlemen: the one and only, star of the show, GLOBAL SESSION!
11
+ #
12
+ # Session is designed to act as much like a Hash as possible. You can use
13
+ # most of the methods you would use with Hash: [], has_key?, each, etc. It has a
14
+ # few additional methods that are specific to itself, mostly involving whether
15
+ # it's expired, valid, supports a certain key, etc.
16
+ #
17
+ # Global sessions are versioned, and each version may have its own encoding
18
+ # strategy. This module acts as a namespace for the different versions, each
19
+ # of which is represented by a class in the module. They all inherit
20
+ # from the abstract base class in order to ensure that they are internally
21
+ # compatible with other components of this gem.
22
+ #
23
+ # This module also acts as a façade for reading global session cookies generated
24
+ # by the different versions; it is responsible for detecting the version of
25
+ # a given cookie, then instantiating a suitable session object.
26
+ module GlobalSession::Session
27
+ def self.new(*args)
28
+ V2.new(*args)
29
+ rescue GlobalSession::MalformedCookie => e
30
+ V1.new(*args)
31
+ end
32
+
33
+ def self.decode_cookie(*args)
34
+ V2.decode_cookie(*args)
35
+ rescue GlobalSession::MalformedCookie => e
36
+ V1.decode_cookie(*args)
37
+ end
402
38
  end