ax.rb
516 lines
| 15.9 KiB
| text/x-ruby
|
RubyLexer
|
r2376 | # 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 | ||||