File: ax.rb

package info (click to toggle)
ruby-openid 2.5.0debian-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 1,980 kB
  • ctags: 2,219
  • sloc: ruby: 16,737; xml: 219; sh: 24; makefile: 2
file content (560 lines) | stat: -rw-r--r-- 17,762 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# Implements the OpenID attribute exchange specification, version 1.0

require 'openid/extension'
require 'openid/trustroot'
require 'openid/message'

module OpenID
  module AX

    UNLIMITED_VALUES = "unlimited"
    MINIMUM_SUPPORTED_ALIAS_LENGTH = 32

    # check alias for invalid characters, raise AXError if found
    def self.check_alias(name)
      if name.match(/(,|\.)/)
        raise Error, ("Alias #{name.inspect} must not contain a "\
                      "comma or period.")
      end
    end

    # Raised when data does not comply with AX 1.0 specification
    class Error < ArgumentError
    end

    # Abstract class containing common code for attribute exchange messages
    class AXMessage < Extension
      attr_accessor :ns_alias, :mode, :ns_uri

      NS_URI = 'http://openid.net/srv/ax/1.0'

      begin
        Message.register_namespace_alias(NS_URI, 'ax')
      rescue NamespaceAliasRegistrationError => e
        Util.log(e)
      end

      def initialize
        @ns_alias = 'ax'
        @ns_uri = NS_URI
        @mode = nil
      end

      protected

      # Raise an exception if the mode in the attribute exchange
      # arguments does not match what is expected for this class.
      def check_mode(ax_args)
        actual_mode = ax_args ? ax_args['mode'] : nil
        if actual_mode != @mode
          raise Error, "Expected mode #{mode.inspect}, got #{actual_mode.inspect}"
        end
      end

      def new_args
        {'mode' => @mode}
      end
    end

    # Represents a single attribute in an attribute exchange
    # request. This should be added to an Request object in order to
    # request the attribute.
    #
    # @ivar required: Whether the attribute will be marked as required
    #     when presented to the subject of the attribute exchange
    #     request.
    # @type required: bool
    #
    # @ivar count: How many values of this type to request from the
    #      subject. Defaults to one.
    # @type count: int
    #
    # @ivar type_uri: The identifier that determines what the attribute
    #      represents and how it is serialized. For example, one type URI
    #      representing dates could represent a Unix timestamp in base 10
    #      and another could represent a human-readable string.
    # @type type_uri: str
    #
    # @ivar ns_alias: The name that should be given to this alias in the
    #      request. If it is not supplied, a generic name will be
    #      assigned. For example, if you want to call a Unix timestamp
    #      value 'tstamp', set its alias to that value. If two attributes
    #      in the same message request to use the same alias, the request
    #      will fail to be generated.
    # @type alias: str or NoneType
    class AttrInfo < Object
      attr_reader :type_uri, :count, :ns_alias
      attr_accessor :required
      def initialize(type_uri, ns_alias=nil, required=false, count=1)
        @type_uri = type_uri
        @count = count
        @required = required
        @ns_alias = ns_alias
      end

      def wants_unlimited_values?
        @count == UNLIMITED_VALUES
      end
    end

    # Given a namespace mapping and a string containing a
    # comma-separated list of namespace aliases, return a list of type
    # URIs that correspond to those aliases.
    # namespace_map: OpenID::NamespaceMap
    def self.to_type_uris(namespace_map, alias_list_s)
      return [] if alias_list_s.nil?
      alias_list_s.split(',').inject([]) {|uris, name|
        type_uri = namespace_map.get_namespace_uri(name)
        raise IndexError, "No type defined for attribute name #{name.inspect}" if type_uri.nil?
        uris << type_uri
      }
    end


    # An attribute exchange 'fetch_request' message. This message is
    # sent by a relying party when it wishes to obtain attributes about
    # the subject of an OpenID authentication request.
    class FetchRequest < AXMessage
      attr_reader :requested_attributes
      attr_accessor :update_url

      MODE = 'fetch_request'

      def initialize(update_url = nil)
        super()
        @mode = MODE
        @requested_attributes = {}
        @update_url = update_url
      end

      # Add an attribute to this attribute exchange request.
      # attribute: AttrInfo, the attribute being requested
      # Raises IndexError if the requested attribute is already present
      #   in this request.
      def add(attribute)
        if @requested_attributes[attribute.type_uri]
          raise IndexError, "The attribute #{attribute.type_uri} has already been requested"
        end
        @requested_attributes[attribute.type_uri] = attribute
      end

      # Get the serialized form of this attribute fetch request.
      # returns a hash of the arguments
      def get_extension_args
        aliases = NamespaceMap.new
        required = []
        if_available = []
        ax_args = new_args
        @requested_attributes.each{|type_uri, attribute|
          if attribute.ns_alias
            name = aliases.add_alias(type_uri, attribute.ns_alias)
          else
            name = aliases.add(type_uri)
          end
          if attribute.required
            required << name
          else
            if_available << name
          end
          if attribute.count != 1
            ax_args["count.#{name}"] = attribute.count.to_s
          end
          ax_args["type.#{name}"] = type_uri
        }

        unless required.empty?
          ax_args['required'] = required.join(',')
        end
        unless if_available.empty?
          ax_args['if_available'] = if_available.join(',')
        end
        return ax_args
      end

      # Get the type URIs for all attributes that have been marked
      # as required.
      def get_required_attrs
        @requested_attributes.inject([]) {|required, (type_uri, attribute)|
          if attribute.required
            required << type_uri
          else
            required
          end
        }
      end

      # Extract a FetchRequest from an OpenID message
      # message: OpenID::Message
      # return a FetchRequest or nil if AX arguments are not present
      def self.from_openid_request(oidreq)
        message = oidreq.message
        ax_args = message.get_args(NS_URI)
        return nil if ax_args == {} or ax_args['mode'] != MODE
        req = new
        req.parse_extension_args(ax_args)

        if req.update_url
          realm = message.get_arg(OPENID_NS, 'realm',
                                  message.get_arg(OPENID_NS, 'return_to'))
          if realm.nil? or realm.empty?
            raise Error, "Cannot validate update_url #{req.update_url.inspect} against absent realm"
          end
          tr = TrustRoot::TrustRoot.parse(realm)
          unless tr.validate_url(req.update_url)
            raise Error, "Update URL #{req.update_url.inspect} failed validation against realm #{realm.inspect}"
          end
        end

        return req
      end

      def parse_extension_args(ax_args)
        check_mode(ax_args)

        aliases = NamespaceMap.new

        ax_args.each{|k,v|
          if k.index('type.') == 0
            name = k[5..-1]
            type_uri = v
            aliases.add_alias(type_uri, name)

            count_key = 'count.'+name
            count_s = ax_args[count_key]
            count = 1
            if count_s
              if count_s == UNLIMITED_VALUES
                count = count_s
              else
                count = count_s.to_i
                if count <= 0
                  raise Error, "Invalid value for count #{count_key.inspect}: #{count_s.inspect}"
                end
              end
            end
            add(AttrInfo.new(type_uri, name, false, count))
          end
        }

        required = AX.to_type_uris(aliases, ax_args['required'])
        required.each{|type_uri|
          @requested_attributes[type_uri].required = true
        }
        if_available = AX.to_type_uris(aliases, ax_args['if_available'])
        all_type_uris = required + if_available

        aliases.namespace_uris.each{|type_uri|
          unless all_type_uris.member? type_uri
            raise Error, "Type URI #{type_uri.inspect} was in the request but not present in 'required' or 'if_available'"
          end
        }
        @update_url = ax_args['update_url']
      end

      # return the list of AttrInfo objects contained in the FetchRequest
      def attributes
        @requested_attributes.values
      end

      # return the list of requested attribute type URIs
      def requested_types
        @requested_attributes.keys
      end

      def member?(type_uri)
        ! @requested_attributes[type_uri].nil?
      end

    end

    # Abstract class that implements a message that has attribute
    # keys and values. It contains the common code between
    # fetch_response and store_request.
    class KeyValueMessage < AXMessage
      attr_reader :data
      def initialize
        super()
        @mode = nil
        @data = Hash.new { |hash, key| hash[key] = [] }
      end

      # Add a single value for the given attribute type to the
      # message. If there are already values specified for this type,
      # this value will be sent in addition to the values already
      # specified.
      def add_value(type_uri, value)
        @data[type_uri] = @data[type_uri] << value
      end

      # Set the values for the given attribute type. This replaces
      # any values that have already been set for this attribute.
      def set_values(type_uri, values)
        @data[type_uri] = values
      end

      # Get the extension arguments for the key/value pairs
      # contained in this message.
      def _get_extension_kv_args(aliases = nil)
        aliases = NamespaceMap.new if aliases.nil?

        ax_args = new_args

        @data.each{|type_uri, values|
          name = aliases.add(type_uri)
          ax_args['type.'+name] = type_uri
          if values.size > 1
            ax_args['count.'+name] = values.size.to_s

            values.each_with_index{|value, i|
              key = "value.#{name}.#{i+1}"
              ax_args[key] = value
            }
            # for attributes with only a single value, use a
            # nice shortcut to only show the value w/o the count
          else 
            values.each do |value|
              key = "value.#{name}"
              ax_args[key] = value
            end
          end
        }
        return ax_args
      end

      # Parse attribute exchange key/value arguments into this object.

      def parse_extension_args(ax_args)
        check_mode(ax_args)
        aliases = NamespaceMap.new

        ax_args.each{|k, v|
          if k.index('type.') == 0
            type_uri = v
            name = k[5..-1]

            AX.check_alias(name)
            aliases.add_alias(type_uri,name)
          end
        }

        aliases.each{|type_uri, name|
          count_s = ax_args['count.'+name]
          count = count_s.to_i
          if count_s.nil?
            value = ax_args['value.'+name]
            if value.nil?
              raise IndexError, "Missing #{'value.'+name} in FetchResponse"
            elsif value.empty?
              values = []
            else
              values = [value]
            end
          elsif count_s.to_i == 0
            values = []
          else
            values = (1..count).inject([]){|l,i|
              key = "value.#{name}.#{i}"
              v = ax_args[key]
              raise IndexError, "Missing #{key} in FetchResponse" if v.nil?
              l << v
            }
          end
          @data[type_uri] = values
        }
      end

      # Get a single value for an attribute. If no value was sent
      # for this attribute, use the supplied default. If there is more
      # than one value for this attribute, this method will fail.
      def get_single(type_uri, default = nil)
        values = @data[type_uri]
        return default if values.empty?
        if values.size != 1
          raise Error, "More than one value present for #{type_uri.inspect}"
        else
          return values[0]
        end
      end

      # retrieve the list of values for this attribute
      def get(type_uri)
        @data[type_uri]
      end

      # retrieve the list of values for this attribute
      def [](type_uri)
        @data[type_uri]
      end

      # get the number of responses for this attribute
      def count(type_uri)
        @data[type_uri].size
      end

    end

    # A fetch_response attribute exchange message
    class FetchResponse < KeyValueMessage
      attr_reader :update_url
      # Use the aliases variable to manually add alias names in the response.
      # They'll be returned to the client in the format: 
      #   openid.ax.type.email=http://openid.net/schema/contact/internet/email
      #   openid.ax.value.email=guy@example.com
      attr_accessor :aliases

      def initialize(update_url = nil)
        super()
        @mode = 'fetch_response'
        @update_url = update_url
        @aliases = NamespaceMap.new
      end

      # Serialize this object into arguments in the attribute
      # exchange namespace
      # Takes an optional FetchRequest.  If specified, the response will be
      # validated against this request, and empty responses for requested
      # fields with no data will be sent.
      def get_extension_args(request = nil)
        zero_value_types = []

        if request
          # Validate the data in the context of the request (the
          # same attributes should be present in each, and the
          # counts in the response must be no more than the counts
          # in the request)
          @data.keys.each{|type_uri|
            unless request.member? type_uri
              raise IndexError, "Response attribute not present in request: #{type_uri.inspect}"
            end
          }

          request.attributes.each{|attr_info|
            # Copy the aliases from the request so that reading
            # the response in light of the request is easier
            if attr_info.ns_alias.nil?
              @aliases.add(attr_info.type_uri)
            else
              @aliases.add_alias(attr_info.type_uri, attr_info.ns_alias)
            end
            values = @data[attr_info.type_uri]
            if values.empty? # @data defaults to []
              zero_value_types << attr_info
            end

            if attr_info.count != UNLIMITED_VALUES and attr_info.count < values.size
              raise Error, "More than the number of requested values were specified for #{attr_info.type_uri.inspect}"
            end
          }
        end

        kv_args = _get_extension_kv_args(@aliases)

        # Add the KV args into the response with the args that are
        # unique to the fetch_response
        ax_args = new_args

        zero_value_types.each{|attr_info|
          name = @aliases.get_alias(attr_info.type_uri)
          kv_args['type.' + name] = attr_info.type_uri
          kv_args['count.' + name] = '0'
        }
        update_url = (request and request.update_url or @update_url)
        ax_args['update_url'] = update_url unless update_url.nil?
        ax_args.update(kv_args)
        return ax_args
      end

      def parse_extension_args(ax_args)
        super
        @update_url = ax_args['update_url']
      end

      # Construct a FetchResponse object from an OpenID library
      # SuccessResponse object.
      def self.from_success_response(success_response, signed=true)
        obj = self.new
        if signed
          ax_args = success_response.get_signed_ns(obj.ns_uri)
        else
          ax_args = success_response.message.get_args(obj.ns_uri)
        end

        begin
          obj.parse_extension_args(ax_args)
          return obj
        rescue Error
          return nil
        end
      end
    end

    # A store request attribute exchange message representation
    class StoreRequest < KeyValueMessage

      MODE = 'store_request'

      def initialize
        super
        @mode = MODE
      end

      # Extract a StoreRequest from an OpenID message
      # message: OpenID::Message
      # return a StoreRequest or nil if AX arguments are not present
      def self.from_openid_request(oidreq)
        message = oidreq.message
        ax_args = message.get_args(NS_URI)
        return nil if ax_args.empty? or ax_args['mode'] != MODE
        req = new
        req.parse_extension_args(ax_args)
        req
      end

      def get_extension_args(aliases=nil)
        ax_args = new_args
        kv_args = _get_extension_kv_args(aliases)
        ax_args.update(kv_args)
        return ax_args
      end
    end

    # An indication that the store request was processed along with
    # this OpenID transaction.
    class StoreResponse < AXMessage
      SUCCESS_MODE = 'store_response_success'
      FAILURE_MODE = 'store_response_failure'
      attr_reader :error_message

      def initialize(succeeded = true, error_message = nil)
        super()
        if succeeded and error_message
          raise Error, "Error message included in a success response"
        end
        if succeeded
          @mode = SUCCESS_MODE
        else
          @mode = FAILURE_MODE
        end
        @error_message = error_message
      end

      def self.from_success_response(success_response)
        resp = nil
        ax_args = success_response.message.get_args(NS_URI)
        resp = ax_args.key?('error') ? new(false, ax_args['error']) : new
      end

      def succeeded?
        @mode == SUCCESS_MODE
      end

      def get_extension_args
        ax_args = new_args
        if !succeeded? and error_message
          ax_args['error'] = @error_message
        end
        return ax_args
      end
    end
  end
end