Eric Davis f70be197e0 Unpacked OpenID gem. #699
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2437 e93f8b46-1217-0410-a6f0-8f06a7374b81
2009-02-11 19:06:37 +00:00

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