git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2437 e93f8b46-1217-0410-a6f0-8f06a7374b81
517 lines
16 KiB
Ruby
517 lines
16 KiB
Ruby
# 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'
|
|
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['mode']
|
|
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
|
|
|
|
def initialize(update_url = nil)
|
|
super()
|
|
@mode = 'fetch_request'
|
|
@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 == {}
|
|
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 = {}
|
|
@data.default = []
|
|
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
|
|
ax_args['count.'+name] = values.size.to_s
|
|
|
|
values.each_with_index{|value, i|
|
|
key = "value.#{name}.#{i+1}"
|
|
ax_args[key] = value
|
|
}
|
|
}
|
|
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
|
|
|
|
def initialize(update_url = nil)
|
|
super()
|
|
@mode = 'fetch_response'
|
|
@update_url = update_url
|
|
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)
|
|
aliases = NamespaceMap.new
|
|
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 => e
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
|
|
# A store request attribute exchange message representation
|
|
class StoreRequest < KeyValueMessage
|
|
def initialize
|
|
super
|
|
@mode = 'store_request'
|
|
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 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
|