C0 code coverage information
Generated on Fri Jul 11 15:55:31 -0700 2008 with rcov 0.7.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
1 # Implements the OpenID attribute exchange specification, version 1.0
2
3 require 'openid/extension'
4 require 'openid/trustroot'
5 require 'openid/message'
6
7 module OpenID
8 module AX
9
10 UNLIMITED_VALUES = "unlimited"
11 MINIMUM_SUPPORTED_ALIAS_LENGTH = 32
12
13 # check alias for invalid characters, raise AXError if found
14 def self.check_alias(name)
15 if name.match(/(,|\.)/)
16 raise Error, ("Alias #{name.inspect} must not contain a "\
17 "comma or period.")
18 end
19 end
20
21 # Raised when data does not comply with AX 1.0 specification
22 class Error < ArgumentError
23 end
24
25 # Abstract class containing common code for attribute exchange messages
26 class AXMessage < Extension
27 attr_accessor :ns_alias, :mode, :ns_uri
28
29 NS_URI = 'http://openid.net/srv/ax/1.0'
30 def initialize
31 @ns_alias = 'ax'
32 @ns_uri = NS_URI
33 @mode = nil
34 end
35
36 protected
37
38 # Raise an exception if the mode in the attribute exchange
39 # arguments does not match what is expected for this class.
40 def check_mode(ax_args)
41 actual_mode = ax_args['mode']
42 if actual_mode != @mode
43 raise Error, "Expected mode #{mode.inspect}, got #{actual_mode.inspect}"
44 end
45 end
46
47 def new_args
48 {'mode' => @mode}
49 end
50 end
51
52 # Represents a single attribute in an attribute exchange
53 # request. This should be added to an Request object in order to
54 # request the attribute.
55 #
56 # @ivar required: Whether the attribute will be marked as required
57 # when presented to the subject of the attribute exchange
58 # request.
59 # @type required: bool
60 #
61 # @ivar count: How many values of this type to request from the
62 # subject. Defaults to one.
63 # @type count: int
64 #
65 # @ivar type_uri: The identifier that determines what the attribute
66 # represents and how it is serialized. For example, one type URI
67 # representing dates could represent a Unix timestamp in base 10
68 # and another could represent a human-readable string.
69 # @type type_uri: str
70 #
71 # @ivar ns_alias: The name that should be given to this alias in the
72 # request. If it is not supplied, a generic name will be
73 # assigned. For example, if you want to call a Unix timestamp
74 # value 'tstamp', set its alias to that value. If two attributes
75 # in the same message request to use the same alias, the request
76 # will fail to be generated.
77 # @type alias: str or NoneType
78 class AttrInfo < Object
79 attr_reader :type_uri, :count, :ns_alias
80 attr_accessor :required
81 def initialize(type_uri, ns_alias=nil, required=false, count=1)
82 @type_uri = type_uri
83 @count = count
84 @required = required
85 @ns_alias = ns_alias
86 end
87
88 def wants_unlimited_values?
89 @count == UNLIMITED_VALUES
90 end
91 end
92
93 # Given a namespace mapping and a string containing a
94 # comma-separated list of namespace aliases, return a list of type
95 # URIs that correspond to those aliases.
96 # namespace_map: OpenID::NamespaceMap
97 def self.to_type_uris(namespace_map, alias_list_s)
98 return [] if alias_list_s.nil?
99 alias_list_s.split(',').inject([]) {|uris, name|
100 type_uri = namespace_map.get_namespace_uri(name)
101 raise IndexError, "No type defined for attribute name #{name.inspect}" if type_uri.nil?
102 uris << type_uri
103 }
104 end
105
106
107 # An attribute exchange 'fetch_request' message. This message is
108 # sent by a relying party when it wishes to obtain attributes about
109 # the subject of an OpenID authentication request.
110 class FetchRequest < AXMessage
111 attr_reader :requested_attributes
112 attr_accessor :update_url
113
114 def initialize(update_url = nil)
115 super()
116 @mode = 'fetch_request'
117 @requested_attributes = {}
118 @update_url = update_url
119 end
120
121 # Add an attribute to this attribute exchange request.
122 # attribute: AttrInfo, the attribute being requested
123 # Raises IndexError if the requested attribute is already present
124 # in this request.
125 def add(attribute)
126 if @requested_attributes[attribute.type_uri]
127 raise IndexError, "The attribute #{attribute.type_uri} has already been requested"
128 end
129 @requested_attributes[attribute.type_uri] = attribute
130 end
131
132 # Get the serialized form of this attribute fetch request.
133 # returns a hash of the arguments
134 def get_extension_args
135 aliases = NamespaceMap.new
136 required = []
137 if_available = []
138 ax_args = new_args
139 @requested_attributes.each{|type_uri, attribute|
140 if attribute.ns_alias
141 name = aliases.add_alias(type_uri, attribute.ns_alias)
142 else
143 name = aliases.add(type_uri)
144 end
145 if attribute.required
146 required << name
147 else
148 if_available << name
149 end
150 if attribute.count != 1
151 ax_args["count.#{name}"] = attribute.count.to_s
152 end
153 ax_args["type.#{name}"] = type_uri
154 }
155
156 unless required.empty?
157 ax_args['required'] = required.join(',')
158 end
159 unless if_available.empty?
160 ax_args['if_available'] = if_available.join(',')
161 end
162 return ax_args
163 end
164
165 # Get the type URIs for all attributes that have been marked
166 # as required.
167 def get_required_attrs
168 @requested_attributes.inject([]) {|required, (type_uri, attribute)|
169 if attribute.required
170 required << type_uri
171 else
172 required
173 end
174 }
175 end
176
177 # Extract a FetchRequest from an OpenID message
178 # message: OpenID::Message
179 # return a FetchRequest or nil if AX arguments are not present
180 def self.from_openid_request(oidreq)
181 message = oidreq.message
182 ax_args = message.get_args(NS_URI)
183 return nil if ax_args == {}
184 req = new
185 req.parse_extension_args(ax_args)
186
187 if req.update_url
188 realm = message.get_arg(OPENID_NS, 'realm',
189 message.get_arg(OPENID_NS, 'return_to'))
190 if realm.nil? or realm.empty?
191 raise Error, "Cannot validate update_url #{req.update_url.inspect} against absent realm"
192 end
193 tr = TrustRoot::TrustRoot.parse(realm)
194 unless tr.validate_url(req.update_url)
195 raise Error, "Update URL #{req.update_url.inspect} failed validation against realm #{realm.inspect}"
196 end
197 end
198
199 return req
200 end
201
202 def parse_extension_args(ax_args)
203 check_mode(ax_args)
204
205 aliases = NamespaceMap.new
206
207 ax_args.each{|k,v|
208 if k.index('type.') == 0
209 name = k[5..-1]
210 type_uri = v
211 aliases.add_alias(type_uri, name)
212
213 count_key = 'count.'+name
214 count_s = ax_args[count_key]
215 count = 1
216 if count_s
217 if count_s == UNLIMITED_VALUES
218 count = count_s
219 else
220 count = count_s.to_i
221 if count <= 0
222 raise Error, "Invalid value for count #{count_key.inspect}: #{count_s.inspect}"
223 end
224 end
225 end
226 add(AttrInfo.new(type_uri, name, false, count))
227 end
228 }
229
230 required = AX.to_type_uris(aliases, ax_args['required'])
231 required.each{|type_uri|
232 @requested_attributes[type_uri].required = true
233 }
234 if_available = AX.to_type_uris(aliases, ax_args['if_available'])
235 all_type_uris = required + if_available
236
237 aliases.namespace_uris.each{|type_uri|
238 unless all_type_uris.member? type_uri
239 raise Error, "Type URI #{type_uri.inspect} was in the request but not present in 'required' or 'if_available'"
240 end
241 }
242 @update_url = ax_args['update_url']
243 end
244
245 # return the list of AttrInfo objects contained in the FetchRequest
246 def attributes
247 @requested_attributes.values
248 end
249
250 # return the list of requested attribute type URIs
251 def requested_types
252 @requested_attributes.keys
253 end
254
255 def member?(type_uri)
256 ! @requested_attributes[type_uri].nil?
257 end
258
259 end
260
261 # Abstract class that implements a message that has attribute
262 # keys and values. It contains the common code between
263 # fetch_response and store_request.
264 class KeyValueMessage < AXMessage
265 attr_reader :data
266 def initialize
267 super()
268 @mode = nil
269 @data = {}
270 @data.default = []
271 end
272
273 # Add a single value for the given attribute type to the
274 # message. If there are already values specified for this type,
275 # this value will be sent in addition to the values already
276 # specified.
277 def add_value(type_uri, value)
278 @data[type_uri] = @data[type_uri] << value
279 end
280
281 # Set the values for the given attribute type. This replaces
282 # any values that have already been set for this attribute.
283 def set_values(type_uri, values)
284 @data[type_uri] = values
285 end
286
287 # Get the extension arguments for the key/value pairs
288 # contained in this message.
289 def _get_extension_kv_args(aliases = nil)
290 aliases = NamespaceMap.new if aliases.nil?
291
292 ax_args = new_args
293
294 @data.each{|type_uri, values|
295 name = aliases.add(type_uri)
296 ax_args['type.'+name] = type_uri
297 ax_args['count.'+name] = values.size.to_s
298
299 values.each_with_index{|value, i|
300 key = "value.#{name}.#{i+1}"
301 ax_args[key] = value
302 }
303 }
304 return ax_args
305 end
306
307 # Parse attribute exchange key/value arguments into this object.
308
309 def parse_extension_args(ax_args)
310 check_mode(ax_args)
311 aliases = NamespaceMap.new
312
313 ax_args.each{|k, v|
314 if k.index('type.') == 0
315 type_uri = v
316 name = k[5..-1]
317
318 AX.check_alias(name)
319 aliases.add_alias(type_uri,name)
320 end
321 }
322
323 aliases.each{|type_uri, name|
324 count_s = ax_args['count.'+name]
325 count = count_s.to_i
326 if count_s.nil?
327 value = ax_args['value.'+name]
328 if value.nil?
329 raise IndexError, "Missing #{'value.'+name} in FetchResponse"
330 elsif value.empty?
331 values = []
332 else
333 values = [value]
334 end
335 elsif count_s.to_i == 0
336 values = []
337 else
338 values = (1..count).inject([]){|l,i|
339 key = "value.#{name}.#{i}"
340 v = ax_args[key]
341 raise IndexError, "Missing #{key} in FetchResponse" if v.nil?
342 l << v
343 }
344 end
345 @data[type_uri] = values
346 }
347 end
348
349 # Get a single value for an attribute. If no value was sent
350 # for this attribute, use the supplied default. If there is more
351 # than one value for this attribute, this method will fail.
352 def get_single(type_uri, default = nil)
353 values = @data[type_uri]
354 return default if values.empty?
355 if values.size != 1
356 raise Error, "More than one value present for #{type_uri.inspect}"
357 else
358 return values[0]
359 end
360 end
361
362 # retrieve the list of values for this attribute
363 def get(type_uri)
364 @data[type_uri]
365 end
366
367 # retrieve the list of values for this attribute
368 def [](type_uri)
369 @data[type_uri]
370 end
371
372 # get the number of responses for this attribute
373 def count(type_uri)
374 @data[type_uri].size
375 end
376
377 end
378
379 # A fetch_response attribute exchange message
380 class FetchResponse < KeyValueMessage
381 attr_reader :update_url
382
383 def initialize(update_url = nil)
384 super()
385 @mode = 'fetch_response'
386 @update_url = update_url
387 end
388
389 # Serialize this object into arguments in the attribute
390 # exchange namespace
391 # Takes an optional FetchRequest. If specified, the response will be
392 # validated against this request, and empty responses for requested
393 # fields with no data will be sent.
394 def get_extension_args(request = nil)
395 aliases = NamespaceMap.new
396 zero_value_types = []
397
398 if request
399 # Validate the data in the context of the request (the
400 # same attributes should be present in each, and the
401 # counts in the response must be no more than the counts
402 # in the request)
403 @data.keys.each{|type_uri|
404 unless request.member? type_uri
405 raise IndexError, "Response attribute not present in request: #{type_uri.inspect}"
406 end
407 }
408
409 request.attributes.each{|attr_info|
410 # Copy the aliases from the request so that reading
411 # the response in light of the request is easier
412 if attr_info.ns_alias.nil?
413 aliases.add(attr_info.type_uri)
414 else
415 aliases.add_alias(attr_info.type_uri, attr_info.ns_alias)
416 end
417 values = @data[attr_info.type_uri]
418 if values.empty? # @data defaults to []
419 zero_value_types << attr_info
420 end
421 if attr_info.count != UNLIMITED_VALUES and attr_info.count < values.size
422 raise Error, "More than the number of requested values were specified for #{attr_info.type_uri.inspect}"
423 end
424 }
425 end
426
427 kv_args = _get_extension_kv_args(aliases)
428
429 # Add the KV args into the response with the args that are
430 # unique to the fetch_response
431 ax_args = new_args
432
433 zero_value_types.each{|attr_info|
434 name = aliases.get_alias(attr_info.type_uri)
435 kv_args['type.' + name] = attr_info.type_uri
436 kv_args['count.' + name] = '0'
437 }
438 update_url = (request and request.update_url or @update_url)
439 ax_args['update_url'] = update_url unless update_url.nil?
440 ax_args.update(kv_args)
441 return ax_args
442 end
443
444 def parse_extension_args(ax_args)
445 super
446 @update_url = ax_args['update_url']
447 end
448
449 # Construct a FetchResponse object from an OpenID library
450 # SuccessResponse object.
451 def self.from_success_response(success_response, signed=true)
452 obj = self.new
453 if signed
454 ax_args = success_response.get_signed_ns(obj.ns_uri)
455 else
456 ax_args = success_response.message.get_args(obj.ns_uri)
457 end
458
459 begin
460 obj.parse_extension_args(ax_args)
461 return obj
462 rescue Error => e
463 return nil
464 end
465 end
466 end
467
468 # A store request attribute exchange message representation
469 class StoreRequest < KeyValueMessage
470 def initialize
471 super
472 @mode = 'store_request'
473 end
474
475 def get_extension_args(aliases=nil)
476 ax_args = new_args
477 kv_args = _get_extension_kv_args(aliases)
478 ax_args.update(kv_args)
479 return ax_args
480 end
481 end
482
483 # An indication that the store request was processed along with
484 # this OpenID transaction.
485 class StoreResponse < AXMessage
486 SUCCESS_MODE = 'store_response_success'
487 FAILURE_MODE = 'store_response_failure'
488 attr_reader :error_message
489
490 def initialize(succeeded = true, error_message = nil)
491 super()
492 if succeeded and error_message
493 raise Error, "Error message included in a success response"
494 end
495 if succeeded
496 @mode = SUCCESS_MODE
497 else
498 @mode = FAILURE_MODE
499 end
500 @error_message = error_message
501 end
502
503 def succeeded?
504 @mode == SUCCESS_MODE
505 end
506
507 def get_extension_args
508 ax_args = new_args
509 if !succeeded? and error_message
510 ax_args['error'] = @error_message
511 end
512 return ax_args
513 end
514 end
515 end
516 end
Generated using the rcov code coverage analysis tool for Ruby version 0.7.0.