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.
@@ -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