Integrated my history into data
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
"""OpenID Extension modules."""
|
||||
|
||||
__all__ = ['ax', 'pape', 'sreg']
|
||||
|
||||
from openid.extensions.draft import pape5 as pape
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,781 @@
|
||||
# -*- test-case-name: openid.test.test_ax -*-
|
||||
"""Implements the OpenID Attribute Exchange specification, version 1.0.
|
||||
|
||||
@since: 2.1.0
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
'AttributeRequest',
|
||||
'FetchRequest',
|
||||
'FetchResponse',
|
||||
'StoreRequest',
|
||||
'StoreResponse',
|
||||
]
|
||||
|
||||
from openid import extension
|
||||
from openid.server.trustroot import TrustRoot
|
||||
from openid.message import NamespaceMap, OPENID_NS
|
||||
|
||||
# Use this as the 'count' value for an attribute in a FetchRequest to
|
||||
# ask for as many values as the OP can provide.
|
||||
UNLIMITED_VALUES = "unlimited"
|
||||
|
||||
# Minimum supported alias length in characters. Here for
|
||||
# completeness.
|
||||
MINIMUM_SUPPORTED_ALIAS_LENGTH = 32
|
||||
|
||||
|
||||
def checkAlias(alias):
|
||||
"""
|
||||
Check an alias for invalid characters; raise AXError if any are
|
||||
found. Return None if the alias is valid.
|
||||
"""
|
||||
if ',' in alias:
|
||||
raise AXError("Alias %r must not contain comma" % (alias, ))
|
||||
if '.' in alias:
|
||||
raise AXError("Alias %r must not contain period" % (alias, ))
|
||||
|
||||
|
||||
class AXError(ValueError):
|
||||
"""Results from data that does not meet the attribute exchange 1.0
|
||||
specification"""
|
||||
|
||||
|
||||
class NotAXMessage(AXError):
|
||||
"""Raised when there is no Attribute Exchange mode in the message."""
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
class AXMessage(extension.Extension):
|
||||
"""Abstract class containing common code for attribute exchange messages
|
||||
|
||||
@cvar ns_alias: The preferred namespace alias for attribute
|
||||
exchange messages
|
||||
|
||||
@cvar mode: The type of this attribute exchange message. This must
|
||||
be overridden in subclasses.
|
||||
"""
|
||||
|
||||
# This class is abstract, so it's OK that it doesn't override the
|
||||
# abstract method in Extension:
|
||||
#
|
||||
#pylint:disable-msg=W0223
|
||||
|
||||
ns_alias = 'ax'
|
||||
ns_uri = 'http://openid.net/srv/ax/1.0'
|
||||
mode = None # NOTE mode is only ever set to a str value, see below
|
||||
|
||||
def _checkMode(self, ax_args):
|
||||
"""Raise an exception if the mode in the attribute exchange
|
||||
arguments does not match what is expected for this class.
|
||||
|
||||
@raises NotAXMessage: When there is no mode value in ax_args at all.
|
||||
|
||||
@raises AXError: When mode does not match.
|
||||
"""
|
||||
mode = ax_args.get('mode')
|
||||
if isinstance(mode, bytes):
|
||||
mode = str(mode, encoding="utf-8")
|
||||
if mode != self.mode:
|
||||
if not mode:
|
||||
raise NotAXMessage()
|
||||
else:
|
||||
raise AXError('Expected mode %r; got %r' % (self.mode, mode))
|
||||
|
||||
def _newArgs(self):
|
||||
"""Return a set of attribute exchange arguments containing the
|
||||
basic information that must be in every attribute exchange
|
||||
message.
|
||||
"""
|
||||
return {'mode': self.mode}
|
||||
|
||||
|
||||
class AttrInfo(object):
|
||||
"""Represents a single attribute in an attribute exchange
|
||||
request. This should be added to an AXRequest 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 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
|
||||
"""
|
||||
|
||||
# It's OK that this class doesn't have public methods (it's just a
|
||||
# holder for a bunch of attributes):
|
||||
#
|
||||
#pylint:disable-msg=R0903
|
||||
|
||||
def __init__(self, type_uri, count=1, required=False, alias=None):
|
||||
self.required = required
|
||||
self.count = count
|
||||
self.type_uri = type_uri
|
||||
self.alias = alias
|
||||
|
||||
if self.alias is not None:
|
||||
checkAlias(self.alias)
|
||||
|
||||
def wantsUnlimitedValues(self):
|
||||
"""
|
||||
When processing a request for this attribute, the OP should
|
||||
call this method to determine whether all available attribute
|
||||
values were requested. If self.count == UNLIMITED_VALUES,
|
||||
this returns True. Otherwise this returns False, in which
|
||||
case self.count is an integer.
|
||||
"""
|
||||
return self.count == UNLIMITED_VALUES
|
||||
|
||||
|
||||
def toTypeURIs(namespace_map, alias_list_s):
|
||||
"""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.
|
||||
|
||||
@param namespace_map: The mapping from namespace URI to alias
|
||||
@type namespace_map: openid.message.NamespaceMap
|
||||
|
||||
@param alias_list_s: The string containing the comma-separated
|
||||
list of aliases. May also be None for convenience.
|
||||
@type alias_list_s: str or NoneType
|
||||
|
||||
@returns: The list of namespace URIs that corresponds to the
|
||||
supplied list of aliases. If the string was zero-length or
|
||||
None, an empty list will be returned.
|
||||
|
||||
@raise KeyError: If an alias is present in the list of aliases but
|
||||
is not present in the namespace map.
|
||||
"""
|
||||
uris = []
|
||||
|
||||
if alias_list_s:
|
||||
for alias in alias_list_s.split(','):
|
||||
type_uri = namespace_map.getNamespaceURI(alias)
|
||||
if type_uri is None:
|
||||
raise KeyError('No type is defined for attribute name %r' %
|
||||
(alias, ))
|
||||
else:
|
||||
uris.append(type_uri)
|
||||
|
||||
return uris
|
||||
|
||||
|
||||
class FetchRequest(AXMessage):
|
||||
"""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.
|
||||
|
||||
@ivar requested_attributes: The attributes that have been
|
||||
requested thus far, indexed by the type URI.
|
||||
@type requested_attributes: {str:AttrInfo}
|
||||
|
||||
@ivar update_url: A URL that will accept responses for this
|
||||
attribute exchange request, even in the absence of the user
|
||||
who made this request.
|
||||
"""
|
||||
mode = 'fetch_request'
|
||||
|
||||
def __init__(self, update_url=None):
|
||||
AXMessage.__init__(self)
|
||||
self.requested_attributes = {}
|
||||
self.update_url = update_url
|
||||
|
||||
def add(self, attribute):
|
||||
"""Add an attribute to this attribute exchange request.
|
||||
|
||||
@param attribute: The attribute that is being requested
|
||||
@type attribute: C{L{AttrInfo}}
|
||||
|
||||
@returns: None
|
||||
|
||||
@raise KeyError: when the requested attribute is already
|
||||
present in this fetch request.
|
||||
"""
|
||||
if attribute.type_uri in self.requested_attributes:
|
||||
raise KeyError('The attribute %r has already been requested' %
|
||||
(attribute.type_uri, ))
|
||||
|
||||
self.requested_attributes[attribute.type_uri] = attribute
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""Get the serialized form of this attribute fetch request.
|
||||
|
||||
@returns: The fetch request message parameters
|
||||
@rtype: {unicode:unicode}
|
||||
"""
|
||||
aliases = NamespaceMap()
|
||||
|
||||
required = []
|
||||
if_available = []
|
||||
|
||||
ax_args = self._newArgs()
|
||||
|
||||
for type_uri, attribute in self.requested_attributes.items():
|
||||
if attribute.alias is None:
|
||||
alias = aliases.add(type_uri)
|
||||
else:
|
||||
# This will raise an exception when the second
|
||||
# attribute with the same alias is added. I think it
|
||||
# would be better to complain at the time that the
|
||||
# attribute is added to this object so that the code
|
||||
# that is adding it is identified in the stack trace,
|
||||
# but it's more work to do so, and it won't be 100%
|
||||
# accurate anyway, since the attributes are
|
||||
# mutable. So for now, just live with the fact that
|
||||
# we'll learn about the error later.
|
||||
#
|
||||
# The other possible approach is to hide the error and
|
||||
# generate a new alias on the fly. I think that would
|
||||
# probably be bad.
|
||||
alias = aliases.addAlias(type_uri, attribute.alias)
|
||||
|
||||
if attribute.required:
|
||||
required.append(alias)
|
||||
else:
|
||||
if_available.append(alias)
|
||||
|
||||
if attribute.count != 1:
|
||||
ax_args['count.' + alias] = str(attribute.count)
|
||||
|
||||
ax_args['type.' + alias] = type_uri
|
||||
|
||||
if required:
|
||||
ax_args['required'] = ','.join(required)
|
||||
|
||||
if if_available:
|
||||
ax_args['if_available'] = ','.join(if_available)
|
||||
|
||||
return ax_args
|
||||
|
||||
def getRequiredAttrs(self):
|
||||
"""Get the type URIs for all attributes that have been marked
|
||||
as required.
|
||||
|
||||
@returns: A list of the type URIs for attributes that have
|
||||
been marked as required.
|
||||
@rtype: [str]
|
||||
"""
|
||||
required = []
|
||||
for type_uri, attribute in self.requested_attributes.items():
|
||||
if attribute.required:
|
||||
required.append(type_uri)
|
||||
|
||||
return required
|
||||
|
||||
def fromOpenIDRequest(cls, openid_request):
|
||||
"""Extract a FetchRequest from an OpenID message
|
||||
|
||||
@param openid_request: The OpenID authentication request
|
||||
containing the attribute fetch request
|
||||
@type openid_request: C{L{openid.server.server.CheckIDRequest}}
|
||||
|
||||
@rtype: C{L{FetchRequest}} or C{None}
|
||||
@returns: The FetchRequest extracted from the message or None, if
|
||||
the message contained no AX extension.
|
||||
|
||||
@raises KeyError: if the AuthRequest is not consistent in its use
|
||||
of namespace aliases.
|
||||
|
||||
@raises AXError: When parseExtensionArgs would raise same.
|
||||
|
||||
@see: L{parseExtensionArgs}
|
||||
"""
|
||||
message = openid_request.message
|
||||
ax_args = message.getArgs(cls.ns_uri)
|
||||
self = cls()
|
||||
try:
|
||||
self.parseExtensionArgs(ax_args)
|
||||
except NotAXMessage as err:
|
||||
return None
|
||||
|
||||
if self.update_url:
|
||||
# Update URL must match the openid.realm of the underlying
|
||||
# OpenID 2 message.
|
||||
realm = message.getArg(OPENID_NS, 'realm',
|
||||
message.getArg(OPENID_NS, 'return_to'))
|
||||
|
||||
if not realm:
|
||||
raise AXError(
|
||||
("Cannot validate update_url %r " + "against absent realm")
|
||||
% (self.update_url, ))
|
||||
|
||||
tr = TrustRoot.parse(realm)
|
||||
if not tr.validateURL(self.update_url):
|
||||
raise AXError(
|
||||
"Update URL %r failed validation against realm %r" %
|
||||
(self.update_url, realm, ))
|
||||
|
||||
return self
|
||||
|
||||
fromOpenIDRequest = classmethod(fromOpenIDRequest)
|
||||
|
||||
def parseExtensionArgs(self, ax_args):
|
||||
"""Given attribute exchange arguments, populate this FetchRequest.
|
||||
|
||||
@param ax_args: Attribute Exchange arguments from the request.
|
||||
As returned from L{Message.getArgs<openid.message.Message.getArgs>}.
|
||||
@type ax_args: dict
|
||||
|
||||
@raises KeyError: if the message is not consistent in its use
|
||||
of namespace aliases.
|
||||
|
||||
@raises NotAXMessage: If ax_args does not include an Attribute Exchange
|
||||
mode.
|
||||
|
||||
@raises AXError: If the data to be parsed does not follow the
|
||||
attribute exchange specification. At least when
|
||||
'if_available' or 'required' is not specified for a
|
||||
particular attribute type.
|
||||
"""
|
||||
# Raises an exception if the mode is not the expected value
|
||||
self._checkMode(ax_args)
|
||||
|
||||
aliases = NamespaceMap()
|
||||
|
||||
for key, value in ax_args.items():
|
||||
if key.startswith('type.'):
|
||||
alias = key[5:]
|
||||
type_uri = value
|
||||
aliases.addAlias(type_uri, alias)
|
||||
|
||||
count_key = 'count.' + alias
|
||||
count_s = ax_args.get(count_key)
|
||||
if count_s:
|
||||
try:
|
||||
count = int(count_s)
|
||||
if count <= 0:
|
||||
raise AXError(
|
||||
"Count %r must be greater than zero, got %r" %
|
||||
(count_key, count_s, ))
|
||||
except ValueError:
|
||||
if count_s != UNLIMITED_VALUES:
|
||||
raise AXError("Invalid count value for %r: %r" %
|
||||
(count_key, count_s, ))
|
||||
count = count_s
|
||||
else:
|
||||
count = 1
|
||||
|
||||
self.add(AttrInfo(type_uri, alias=alias, count=count))
|
||||
|
||||
required = toTypeURIs(aliases, ax_args.get('required'))
|
||||
|
||||
for type_uri in required:
|
||||
self.requested_attributes[type_uri].required = True
|
||||
|
||||
if_available = toTypeURIs(aliases, ax_args.get('if_available'))
|
||||
|
||||
all_type_uris = required + if_available
|
||||
|
||||
for type_uri in aliases.iterNamespaceURIs():
|
||||
if type_uri not in all_type_uris:
|
||||
raise AXError('Type URI %r was in the request but not '
|
||||
'present in "required" or "if_available"' %
|
||||
(type_uri, ))
|
||||
|
||||
self.update_url = ax_args.get('update_url')
|
||||
|
||||
def iterAttrs(self):
|
||||
"""Iterate over the AttrInfo objects that are
|
||||
contained in this fetch_request.
|
||||
"""
|
||||
return iter(self.requested_attributes.values())
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the attribute type URIs in this fetch_request
|
||||
"""
|
||||
return iter(self.requested_attributes)
|
||||
|
||||
def has_key(self, type_uri):
|
||||
"""Is the given type URI present in this fetch_request?
|
||||
"""
|
||||
return type_uri in self.requested_attributes
|
||||
|
||||
__contains__ = has_key
|
||||
|
||||
|
||||
class AXKeyValueMessage(AXMessage):
|
||||
"""An abstract class that implements a message that has attribute
|
||||
keys and values. It contains the common code between
|
||||
fetch_response and store_request.
|
||||
"""
|
||||
|
||||
# This class is abstract, so it's OK that it doesn't override the
|
||||
# abstract method in Extension:
|
||||
#
|
||||
#pylint:disable-msg=W0223
|
||||
|
||||
def __init__(self):
|
||||
AXMessage.__init__(self)
|
||||
self.data = {}
|
||||
|
||||
def addValue(self, type_uri, value):
|
||||
"""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.
|
||||
|
||||
@param type_uri: The URI for the attribute
|
||||
|
||||
@param value: The value to add to the response to the relying
|
||||
party for this attribute
|
||||
@type value: unicode
|
||||
|
||||
@returns: None
|
||||
"""
|
||||
try:
|
||||
values = self.data[type_uri]
|
||||
except KeyError:
|
||||
values = self.data[type_uri] = []
|
||||
|
||||
values.append(value)
|
||||
|
||||
def setValues(self, type_uri, values):
|
||||
"""Set the values for the given attribute type. This replaces
|
||||
any values that have already been set for this attribute.
|
||||
|
||||
@param type_uri: The URI for the attribute
|
||||
|
||||
@param values: A list of values to send for this attribute.
|
||||
@type values: [unicode]
|
||||
"""
|
||||
|
||||
self.data[type_uri] = values
|
||||
|
||||
def _getExtensionKVArgs(self, aliases=None):
|
||||
"""Get the extension arguments for the key/value pairs
|
||||
contained in this message.
|
||||
|
||||
@param aliases: An alias mapping. Set to None if you don't
|
||||
care about the aliases for this request.
|
||||
"""
|
||||
if aliases is None:
|
||||
aliases = NamespaceMap()
|
||||
|
||||
ax_args = {}
|
||||
|
||||
for type_uri, values in self.data.items():
|
||||
alias = aliases.add(type_uri)
|
||||
|
||||
ax_args['type.' + alias] = type_uri
|
||||
ax_args['count.' + alias] = str(len(values))
|
||||
|
||||
for i, value in enumerate(values):
|
||||
key = 'value.%s.%d' % (alias, i + 1)
|
||||
ax_args[key] = value
|
||||
|
||||
return ax_args
|
||||
|
||||
def parseExtensionArgs(self, ax_args):
|
||||
"""Parse attribute exchange key/value arguments into this
|
||||
object.
|
||||
|
||||
@param ax_args: The attribute exchange fetch_response
|
||||
arguments, with namespacing removed.
|
||||
@type ax_args: {unicode:unicode}
|
||||
|
||||
@returns: None
|
||||
|
||||
@raises ValueError: If the message has bad values for
|
||||
particular fields
|
||||
|
||||
@raises KeyError: If the namespace mapping is bad or required
|
||||
arguments are missing
|
||||
"""
|
||||
self._checkMode(ax_args)
|
||||
|
||||
aliases = NamespaceMap()
|
||||
|
||||
for key, value in ax_args.items():
|
||||
if key.startswith('type.'):
|
||||
type_uri = value
|
||||
alias = key[5:]
|
||||
checkAlias(alias)
|
||||
aliases.addAlias(type_uri, alias)
|
||||
|
||||
for type_uri, alias in aliases.items():
|
||||
try:
|
||||
count_s = ax_args['count.' + alias]
|
||||
except KeyError:
|
||||
value = ax_args['value.' + alias]
|
||||
|
||||
if value == '':
|
||||
values = []
|
||||
else:
|
||||
values = [value]
|
||||
else:
|
||||
count = int(count_s)
|
||||
values = []
|
||||
for i in range(1, count + 1):
|
||||
value_key = 'value.%s.%d' % (alias, i)
|
||||
value = ax_args[value_key]
|
||||
values.append(value)
|
||||
|
||||
self.data[type_uri] = values
|
||||
|
||||
def getSingle(self, type_uri, default=None):
|
||||
"""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.
|
||||
|
||||
@type type_uri: str
|
||||
@param type_uri: The URI for the attribute
|
||||
|
||||
@param default: The value to return if the attribute was not
|
||||
sent in the fetch_response.
|
||||
|
||||
@returns: The value of the attribute in the fetch_response
|
||||
message, or the default supplied
|
||||
@rtype: unicode or NoneType
|
||||
|
||||
@raises ValueError: If there is more than one value for this
|
||||
parameter in the fetch_response message.
|
||||
@raises KeyError: If the attribute was not sent in this response
|
||||
"""
|
||||
values = self.data.get(type_uri)
|
||||
if not values:
|
||||
return default
|
||||
elif len(values) == 1:
|
||||
return values[0]
|
||||
else:
|
||||
raise AXError('More than one value present for %r' % (type_uri, ))
|
||||
|
||||
def get(self, type_uri):
|
||||
"""Get the list of values for this attribute in the
|
||||
fetch_response.
|
||||
|
||||
XXX: what to do if the values are not present? default
|
||||
parameter? this is funny because it's always supposed to
|
||||
return a list, so the default may break that, though it's
|
||||
provided by the user's code, so it might be okay. If no
|
||||
default is supplied, should the return be None or []?
|
||||
|
||||
@param type_uri: The URI of the attribute
|
||||
|
||||
@returns: The list of values for this attribute in the
|
||||
response. May be an empty list.
|
||||
@rtype: [unicode]
|
||||
|
||||
@raises KeyError: If the attribute was not sent in the response
|
||||
"""
|
||||
return self.data[type_uri]
|
||||
|
||||
def count(self, type_uri):
|
||||
"""Get the number of responses for a particular attribute in
|
||||
this fetch_response message.
|
||||
|
||||
@param type_uri: The URI of the attribute
|
||||
|
||||
@returns: The number of values sent for this attribute
|
||||
|
||||
@raises KeyError: If the attribute was not sent in the
|
||||
response. KeyError will not be raised if the number of
|
||||
values was zero.
|
||||
"""
|
||||
return len(self.get(type_uri))
|
||||
|
||||
|
||||
class FetchResponse(AXKeyValueMessage):
|
||||
"""A fetch_response attribute exchange message
|
||||
"""
|
||||
mode = 'fetch_response'
|
||||
|
||||
def __init__(self, request=None, update_url=None):
|
||||
"""
|
||||
@param request: When supplied, I will use namespace aliases
|
||||
that match those in this request. I will also check to
|
||||
make sure I do not respond with attributes that were not
|
||||
requested.
|
||||
|
||||
@type request: L{FetchRequest}
|
||||
|
||||
@param update_url: By default, C{update_url} is taken from the
|
||||
request. But if you do not supply the request, you may set
|
||||
the C{update_url} here.
|
||||
|
||||
@type update_url: str
|
||||
"""
|
||||
AXKeyValueMessage.__init__(self)
|
||||
self.update_url = update_url
|
||||
self.request = request
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""Serialize this object into arguments in the attribute
|
||||
exchange namespace
|
||||
|
||||
@returns: The dictionary of unqualified attribute exchange
|
||||
arguments that represent this fetch_response.
|
||||
@rtype: {unicode;unicode}
|
||||
"""
|
||||
|
||||
aliases = NamespaceMap()
|
||||
|
||||
zero_value_types = []
|
||||
|
||||
if self.request is not None:
|
||||
# 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)
|
||||
|
||||
for type_uri in self.data:
|
||||
if type_uri not in self.request:
|
||||
raise KeyError(
|
||||
'Response attribute not present in request: %r' %
|
||||
(type_uri, ))
|
||||
|
||||
for attr_info in self.request.iterAttrs():
|
||||
# Copy the aliases from the request so that reading
|
||||
# the response in light of the request is easier
|
||||
if attr_info.alias is None:
|
||||
aliases.add(attr_info.type_uri)
|
||||
else:
|
||||
aliases.addAlias(attr_info.type_uri, attr_info.alias)
|
||||
|
||||
try:
|
||||
values = self.data[attr_info.type_uri]
|
||||
except KeyError:
|
||||
values = []
|
||||
zero_value_types.append(attr_info)
|
||||
|
||||
if (attr_info.count != UNLIMITED_VALUES) and \
|
||||
(attr_info.count < len(values)):
|
||||
raise AXError(
|
||||
'More than the number of requested values were '
|
||||
'specified for %r' % (attr_info.type_uri, ))
|
||||
|
||||
kv_args = self._getExtensionKVArgs(aliases)
|
||||
|
||||
# Add the KV args into the response with the args that are
|
||||
# unique to the fetch_response
|
||||
ax_args = self._newArgs()
|
||||
|
||||
# For each requested attribute, put its type/alias and count
|
||||
# into the response even if no data were returned.
|
||||
for attr_info in zero_value_types:
|
||||
alias = aliases.getAlias(attr_info.type_uri)
|
||||
kv_args['type.' + alias] = attr_info.type_uri
|
||||
kv_args['count.' + alias] = '0'
|
||||
|
||||
update_url = ((self.request and self.request.update_url) or
|
||||
self.update_url)
|
||||
|
||||
if update_url:
|
||||
ax_args['update_url'] = update_url
|
||||
|
||||
ax_args.update(kv_args)
|
||||
|
||||
return ax_args
|
||||
|
||||
def parseExtensionArgs(self, ax_args):
|
||||
"""@see: {Extension.parseExtensionArgs<openid.extension.Extension.parseExtensionArgs>}"""
|
||||
super(FetchResponse, self).parseExtensionArgs(ax_args)
|
||||
self.update_url = ax_args.get('update_url')
|
||||
|
||||
def fromSuccessResponse(cls, success_response, signed=True):
|
||||
"""Construct a FetchResponse object from an OpenID library
|
||||
SuccessResponse object.
|
||||
|
||||
@param success_response: A successful id_res response object
|
||||
@type success_response: openid.consumer.consumer.SuccessResponse
|
||||
|
||||
@param signed: Whether non-signed args should be
|
||||
processsed. If True (the default), only signed arguments
|
||||
will be processsed.
|
||||
@type signed: bool
|
||||
|
||||
@returns: A FetchResponse containing the data from the OpenID
|
||||
message, or None if the SuccessResponse did not contain AX
|
||||
extension data.
|
||||
|
||||
@raises AXError: when the AX data cannot be parsed.
|
||||
"""
|
||||
self = cls()
|
||||
ax_args = success_response.extensionResponse(self.ns_uri, signed)
|
||||
|
||||
try:
|
||||
self.parseExtensionArgs(ax_args)
|
||||
except NotAXMessage as err:
|
||||
return None
|
||||
else:
|
||||
return self
|
||||
|
||||
fromSuccessResponse = classmethod(fromSuccessResponse)
|
||||
|
||||
|
||||
class StoreRequest(AXKeyValueMessage):
|
||||
"""A store request attribute exchange message representation
|
||||
"""
|
||||
mode = 'store_request'
|
||||
|
||||
def __init__(self, aliases=None):
|
||||
"""
|
||||
@param aliases: The namespace aliases to use when making this
|
||||
store request. Leave as None to use defaults.
|
||||
"""
|
||||
super(StoreRequest, self).__init__()
|
||||
self.aliases = aliases
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""
|
||||
@see: L{Extension.getExtensionArgs<openid.extension.Extension.getExtensionArgs>}
|
||||
"""
|
||||
ax_args = self._newArgs()
|
||||
kv_args = self._getExtensionKVArgs(self.aliases)
|
||||
ax_args.update(kv_args)
|
||||
return ax_args
|
||||
|
||||
|
||||
class StoreResponse(AXMessage):
|
||||
"""An indication that the store request was processed along with
|
||||
this OpenID transaction.
|
||||
"""
|
||||
|
||||
SUCCESS_MODE = 'store_response_success'
|
||||
FAILURE_MODE = 'store_response_failure'
|
||||
|
||||
def __init__(self, succeeded=True, error_message=None):
|
||||
AXMessage.__init__(self)
|
||||
|
||||
if succeeded and error_message is not None:
|
||||
raise AXError('An error message may only be included in a '
|
||||
'failing fetch response')
|
||||
if succeeded:
|
||||
self.mode = self.SUCCESS_MODE
|
||||
else:
|
||||
self.mode = self.FAILURE_MODE
|
||||
|
||||
self.error_message = error_message
|
||||
|
||||
def succeeded(self):
|
||||
"""Was this response a success response?"""
|
||||
return self.mode == self.SUCCESS_MODE
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""@see: {Extension.getExtensionArgs<openid.extension.Extension.getExtensionArgs>}"""
|
||||
ax_args = self._newArgs()
|
||||
if not self.succeeded() and self.error_message:
|
||||
ax_args['error'] = self.error_message
|
||||
|
||||
return ax_args
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,285 @@
|
||||
"""An implementation of the OpenID Provider Authentication Policy
|
||||
Extension 1.0
|
||||
|
||||
@see: http://openid.net/developers/specs/
|
||||
|
||||
@since: 2.1.0
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
'Request',
|
||||
'Response',
|
||||
'ns_uri',
|
||||
'AUTH_PHISHING_RESISTANT',
|
||||
'AUTH_MULTI_FACTOR',
|
||||
'AUTH_MULTI_FACTOR_PHYSICAL',
|
||||
]
|
||||
|
||||
from openid.extension import Extension
|
||||
import re
|
||||
|
||||
ns_uri = "http://specs.openid.net/extensions/pape/1.0"
|
||||
|
||||
AUTH_MULTI_FACTOR_PHYSICAL = \
|
||||
'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'
|
||||
AUTH_MULTI_FACTOR = \
|
||||
'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
|
||||
AUTH_PHISHING_RESISTANT = \
|
||||
'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
|
||||
|
||||
TIME_VALIDATOR = re.compile('^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$')
|
||||
|
||||
|
||||
class Request(Extension):
|
||||
"""A Provider Authentication Policy request, sent from a relying
|
||||
party to a provider
|
||||
|
||||
@ivar preferred_auth_policies: The authentication policies that
|
||||
the relying party prefers
|
||||
@type preferred_auth_policies: [str]
|
||||
|
||||
@ivar max_auth_age: The maximum time, in seconds, that the relying
|
||||
party wants to allow to have elapsed before the user must
|
||||
re-authenticate
|
||||
@type max_auth_age: int or NoneType
|
||||
"""
|
||||
|
||||
ns_alias = 'pape'
|
||||
|
||||
def __init__(self, preferred_auth_policies=None, max_auth_age=None):
|
||||
super(Request, self).__init__()
|
||||
if not preferred_auth_policies:
|
||||
preferred_auth_policies = []
|
||||
|
||||
self.preferred_auth_policies = preferred_auth_policies
|
||||
self.max_auth_age = max_auth_age
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.preferred_auth_policies or
|
||||
self.max_auth_age is not None)
|
||||
|
||||
def addPolicyURI(self, policy_uri):
|
||||
"""Add an acceptable authentication policy URI to this request
|
||||
|
||||
This method is intended to be used by the relying party to add
|
||||
acceptable authentication types to the request.
|
||||
|
||||
@param policy_uri: The identifier for the preferred type of
|
||||
authentication.
|
||||
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
|
||||
"""
|
||||
if policy_uri not in self.preferred_auth_policies:
|
||||
self.preferred_auth_policies.append(policy_uri)
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""@see: C{L{Extension.getExtensionArgs}}
|
||||
"""
|
||||
ns_args = {
|
||||
'preferred_auth_policies': ' '.join(self.preferred_auth_policies)
|
||||
}
|
||||
|
||||
if self.max_auth_age is not None:
|
||||
ns_args['max_auth_age'] = str(self.max_auth_age)
|
||||
|
||||
return ns_args
|
||||
|
||||
def fromOpenIDRequest(cls, request):
|
||||
"""Instantiate a Request object from the arguments in a
|
||||
C{checkid_*} OpenID message
|
||||
"""
|
||||
self = cls()
|
||||
args = request.message.getArgs(self.ns_uri)
|
||||
|
||||
if args == {}:
|
||||
return None
|
||||
|
||||
self.parseExtensionArgs(args)
|
||||
return self
|
||||
|
||||
fromOpenIDRequest = classmethod(fromOpenIDRequest)
|
||||
|
||||
def parseExtensionArgs(self, args):
|
||||
"""Set the state of this request to be that expressed in these
|
||||
PAPE arguments
|
||||
|
||||
@param args: The PAPE arguments without a namespace
|
||||
|
||||
@rtype: None
|
||||
|
||||
@raises ValueError: When the max_auth_age is not parseable as
|
||||
an integer
|
||||
"""
|
||||
|
||||
# preferred_auth_policies is a space-separated list of policy URIs
|
||||
self.preferred_auth_policies = []
|
||||
|
||||
policies_str = args.get('preferred_auth_policies')
|
||||
if policies_str:
|
||||
if isinstance(policies_str, bytes):
|
||||
policies_str = str(policies_str, encoding="utf-8")
|
||||
for uri in policies_str.split(' '):
|
||||
if uri not in self.preferred_auth_policies:
|
||||
self.preferred_auth_policies.append(uri)
|
||||
|
||||
# max_auth_age is base-10 integer number of seconds
|
||||
max_auth_age_str = args.get('max_auth_age')
|
||||
self.max_auth_age = None
|
||||
|
||||
if max_auth_age_str:
|
||||
try:
|
||||
self.max_auth_age = int(max_auth_age_str)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def preferredTypes(self, supported_types):
|
||||
"""Given a list of authentication policy URIs that a provider
|
||||
supports, this method returns the subsequence of those types
|
||||
that are preferred by the relying party.
|
||||
|
||||
@param supported_types: A sequence of authentication policy
|
||||
type URIs that are supported by a provider
|
||||
|
||||
@returns: The sub-sequence of the supported types that are
|
||||
preferred by the relying party. This list will be ordered
|
||||
in the order that the types appear in the supported_types
|
||||
sequence, and may be empty if the provider does not prefer
|
||||
any of the supported authentication types.
|
||||
|
||||
@returntype: [str]
|
||||
"""
|
||||
return list(
|
||||
filter(self.preferred_auth_policies.__contains__, supported_types))
|
||||
|
||||
|
||||
Request.ns_uri = ns_uri
|
||||
|
||||
|
||||
class Response(Extension):
|
||||
"""A Provider Authentication Policy response, sent from a provider
|
||||
to a relying party
|
||||
"""
|
||||
|
||||
ns_alias = 'pape'
|
||||
|
||||
def __init__(self,
|
||||
auth_policies=None,
|
||||
auth_time=None,
|
||||
nist_auth_level=None):
|
||||
super(Response, self).__init__()
|
||||
if auth_policies:
|
||||
self.auth_policies = auth_policies
|
||||
else:
|
||||
self.auth_policies = []
|
||||
|
||||
self.auth_time = auth_time
|
||||
self.nist_auth_level = nist_auth_level
|
||||
|
||||
def addPolicyURI(self, policy_uri):
|
||||
"""Add a authentication policy to this response
|
||||
|
||||
This method is intended to be used by the provider to add a
|
||||
policy that the provider conformed to when authenticating the user.
|
||||
|
||||
@param policy_uri: The identifier for the preferred type of
|
||||
authentication.
|
||||
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
|
||||
"""
|
||||
if policy_uri not in self.auth_policies:
|
||||
self.auth_policies.append(policy_uri)
|
||||
|
||||
def fromSuccessResponse(cls, success_response):
|
||||
"""Create a C{L{Response}} object from a successful OpenID
|
||||
library response
|
||||
(C{L{openid.consumer.consumer.SuccessResponse}}) response
|
||||
message
|
||||
|
||||
@param success_response: A SuccessResponse from consumer.complete()
|
||||
@type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
|
||||
|
||||
@rtype: Response or None
|
||||
@returns: A provider authentication policy response from the
|
||||
data that was supplied with the C{id_res} response or None
|
||||
if the provider sent no signed PAPE response arguments.
|
||||
"""
|
||||
self = cls()
|
||||
|
||||
# PAPE requires that the args be signed.
|
||||
args = success_response.getSignedNS(self.ns_uri)
|
||||
|
||||
# Only try to construct a PAPE response if the arguments were
|
||||
# signed in the OpenID response. If not, return None.
|
||||
if args is not None:
|
||||
self.parseExtensionArgs(args)
|
||||
return self
|
||||
else:
|
||||
return None
|
||||
|
||||
def parseExtensionArgs(self, args, strict=False):
|
||||
"""Parse the provider authentication policy arguments into the
|
||||
internal state of this object
|
||||
|
||||
@param args: unqualified provider authentication policy
|
||||
arguments
|
||||
|
||||
@param strict: Whether to raise an exception when bad data is
|
||||
encountered
|
||||
|
||||
@returns: None. The data is parsed into the internal fields of
|
||||
this object.
|
||||
"""
|
||||
policies_str = args.get('auth_policies')
|
||||
if policies_str and policies_str != 'none':
|
||||
self.auth_policies = policies_str.split(' ')
|
||||
|
||||
nist_level_str = args.get('nist_auth_level')
|
||||
if nist_level_str:
|
||||
try:
|
||||
nist_level = int(nist_level_str)
|
||||
except ValueError:
|
||||
if strict:
|
||||
raise ValueError(
|
||||
'nist_auth_level must be an integer between '
|
||||
'zero and four, inclusive')
|
||||
else:
|
||||
self.nist_auth_level = None
|
||||
else:
|
||||
if 0 <= nist_level < 5:
|
||||
self.nist_auth_level = nist_level
|
||||
|
||||
auth_time = args.get('auth_time')
|
||||
if auth_time:
|
||||
if TIME_VALIDATOR.match(auth_time):
|
||||
self.auth_time = auth_time
|
||||
elif strict:
|
||||
raise ValueError("auth_time must be in RFC3339 format")
|
||||
|
||||
fromSuccessResponse = classmethod(fromSuccessResponse)
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""@see: C{L{Extension.getExtensionArgs}}
|
||||
"""
|
||||
if len(self.auth_policies) == 0:
|
||||
ns_args = {
|
||||
'auth_policies': 'none',
|
||||
}
|
||||
else:
|
||||
ns_args = {
|
||||
'auth_policies': ' '.join(self.auth_policies),
|
||||
}
|
||||
|
||||
if self.nist_auth_level is not None:
|
||||
if self.nist_auth_level not in list(range(0, 5)):
|
||||
raise ValueError('nist_auth_level must be an integer between '
|
||||
'zero and four, inclusive')
|
||||
ns_args['nist_auth_level'] = str(self.nist_auth_level)
|
||||
|
||||
if self.auth_time is not None:
|
||||
if not TIME_VALIDATOR.match(self.auth_time):
|
||||
raise ValueError('auth_time must be in RFC3339 format')
|
||||
|
||||
ns_args['auth_time'] = self.auth_time
|
||||
|
||||
return ns_args
|
||||
|
||||
|
||||
Response.ns_uri = ns_uri
|
||||
@@ -0,0 +1,481 @@
|
||||
"""An implementation of the OpenID Provider Authentication Policy
|
||||
Extension 1.0, Draft 5
|
||||
|
||||
@see: http://openid.net/developers/specs/
|
||||
|
||||
@since: 2.1.0
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
'Request',
|
||||
'Response',
|
||||
'ns_uri',
|
||||
'AUTH_PHISHING_RESISTANT',
|
||||
'AUTH_MULTI_FACTOR',
|
||||
'AUTH_MULTI_FACTOR_PHYSICAL',
|
||||
'LEVELS_NIST',
|
||||
'LEVELS_JISA',
|
||||
]
|
||||
|
||||
from openid.extension import Extension
|
||||
import warnings
|
||||
import re
|
||||
|
||||
ns_uri = "http://specs.openid.net/extensions/pape/1.0"
|
||||
|
||||
AUTH_MULTI_FACTOR_PHYSICAL = \
|
||||
'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'
|
||||
AUTH_MULTI_FACTOR = \
|
||||
'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
|
||||
AUTH_PHISHING_RESISTANT = \
|
||||
'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
|
||||
AUTH_NONE = \
|
||||
'http://schemas.openid.net/pape/policies/2007/06/none'
|
||||
|
||||
TIME_VALIDATOR = re.compile('^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$')
|
||||
|
||||
LEVELS_NIST = 'http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf'
|
||||
LEVELS_JISA = 'http://www.jisa.or.jp/spec/auth_level.html'
|
||||
|
||||
|
||||
class PAPEExtension(Extension):
|
||||
_default_auth_level_aliases = {
|
||||
'nist': LEVELS_NIST,
|
||||
'jisa': LEVELS_JISA,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.auth_level_aliases = self._default_auth_level_aliases.copy()
|
||||
|
||||
def _addAuthLevelAlias(self, auth_level_uri, alias=None):
|
||||
"""Add an auth level URI alias to this request.
|
||||
|
||||
@param auth_level_uri: The auth level URI to send in the
|
||||
request.
|
||||
|
||||
@param alias: The namespace alias to use for this auth level
|
||||
in this message. May be None if the alias is not
|
||||
important.
|
||||
"""
|
||||
if alias is None:
|
||||
try:
|
||||
alias = self._getAlias(auth_level_uri)
|
||||
except KeyError:
|
||||
alias = self._generateAlias()
|
||||
else:
|
||||
existing_uri = self.auth_level_aliases.get(alias)
|
||||
if existing_uri is not None and existing_uri != auth_level_uri:
|
||||
raise KeyError('Attempting to redefine alias %r from %r to %r',
|
||||
alias, existing_uri, auth_level_uri)
|
||||
|
||||
self.auth_level_aliases[alias] = auth_level_uri
|
||||
|
||||
def _generateAlias(self):
|
||||
"""Return an unused auth level alias"""
|
||||
for i in range(1000):
|
||||
alias = 'cust%d' % (i, )
|
||||
if alias not in self.auth_level_aliases:
|
||||
return alias
|
||||
|
||||
raise RuntimeError('Could not find an unused alias (tried 1000!)')
|
||||
|
||||
def _getAlias(self, auth_level_uri):
|
||||
"""Return the alias for the specified auth level URI.
|
||||
|
||||
@raises KeyError: if no alias is defined
|
||||
"""
|
||||
for (alias, existing_uri) in self.auth_level_aliases.items():
|
||||
if auth_level_uri == existing_uri:
|
||||
return alias
|
||||
|
||||
raise KeyError(auth_level_uri)
|
||||
|
||||
|
||||
class Request(PAPEExtension):
|
||||
"""A Provider Authentication Policy request, sent from a relying
|
||||
party to a provider
|
||||
|
||||
@ivar preferred_auth_policies: The authentication policies that
|
||||
the relying party prefers
|
||||
@type preferred_auth_policies: [str]
|
||||
|
||||
@ivar max_auth_age: The maximum time, in seconds, that the relying
|
||||
party wants to allow to have elapsed before the user must
|
||||
re-authenticate
|
||||
@type max_auth_age: int or NoneType
|
||||
|
||||
@ivar preferred_auth_level_types: Ordered list of authentication
|
||||
level namespace URIs
|
||||
|
||||
@type preferred_auth_level_types: [str]
|
||||
"""
|
||||
|
||||
ns_alias = 'pape'
|
||||
|
||||
def __init__(self,
|
||||
preferred_auth_policies=None,
|
||||
max_auth_age=None,
|
||||
preferred_auth_level_types=None):
|
||||
super(Request, self).__init__()
|
||||
if preferred_auth_policies is None:
|
||||
preferred_auth_policies = []
|
||||
|
||||
self.preferred_auth_policies = preferred_auth_policies
|
||||
self.max_auth_age = max_auth_age
|
||||
self.preferred_auth_level_types = []
|
||||
|
||||
if preferred_auth_level_types is not None:
|
||||
for auth_level in preferred_auth_level_types:
|
||||
self.addAuthLevel(auth_level)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.preferred_auth_policies or
|
||||
self.max_auth_age is not None or
|
||||
self.preferred_auth_level_types)
|
||||
|
||||
def addPolicyURI(self, policy_uri):
|
||||
"""Add an acceptable authentication policy URI to this request
|
||||
|
||||
This method is intended to be used by the relying party to add
|
||||
acceptable authentication types to the request.
|
||||
|
||||
@param policy_uri: The identifier for the preferred type of
|
||||
authentication.
|
||||
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-05.html#auth_policies
|
||||
"""
|
||||
if policy_uri not in self.preferred_auth_policies:
|
||||
self.preferred_auth_policies.append(policy_uri)
|
||||
|
||||
def addAuthLevel(self, auth_level_uri, alias=None):
|
||||
self._addAuthLevelAlias(auth_level_uri, alias)
|
||||
if auth_level_uri not in self.preferred_auth_level_types:
|
||||
self.preferred_auth_level_types.append(auth_level_uri)
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""@see: C{L{Extension.getExtensionArgs}}
|
||||
"""
|
||||
ns_args = {
|
||||
'preferred_auth_policies': ' '.join(self.preferred_auth_policies),
|
||||
}
|
||||
|
||||
if self.max_auth_age is not None:
|
||||
ns_args['max_auth_age'] = str(self.max_auth_age)
|
||||
|
||||
if self.preferred_auth_level_types:
|
||||
preferred_types = []
|
||||
|
||||
for auth_level_uri in self.preferred_auth_level_types:
|
||||
alias = self._getAlias(auth_level_uri)
|
||||
ns_args['auth_level.ns.%s' % (alias, )] = auth_level_uri
|
||||
preferred_types.append(alias)
|
||||
|
||||
ns_args['preferred_auth_level_types'] = ' '.join(preferred_types)
|
||||
|
||||
return ns_args
|
||||
|
||||
def fromOpenIDRequest(cls, request):
|
||||
"""Instantiate a Request object from the arguments in a
|
||||
C{checkid_*} OpenID message
|
||||
"""
|
||||
self = cls()
|
||||
args = request.message.getArgs(self.ns_uri)
|
||||
is_openid1 = request.message.isOpenID1()
|
||||
|
||||
if args == {}:
|
||||
return None
|
||||
|
||||
self.parseExtensionArgs(args, is_openid1)
|
||||
return self
|
||||
|
||||
fromOpenIDRequest = classmethod(fromOpenIDRequest)
|
||||
|
||||
def parseExtensionArgs(self, args, is_openid1, strict=False):
|
||||
"""Set the state of this request to be that expressed in these
|
||||
PAPE arguments
|
||||
|
||||
@param args: The PAPE arguments without a namespace
|
||||
|
||||
@param strict: Whether to raise an exception if the input is
|
||||
out of spec or otherwise malformed. If strict is false,
|
||||
malformed input will be ignored.
|
||||
|
||||
@param is_openid1: Whether the input should be treated as part
|
||||
of an OpenID1 request
|
||||
|
||||
@rtype: None
|
||||
|
||||
@raises ValueError: When the max_auth_age is not parseable as
|
||||
an integer
|
||||
"""
|
||||
|
||||
# preferred_auth_policies is a space-separated list of policy URIs
|
||||
self.preferred_auth_policies = []
|
||||
|
||||
policies_str = args.get('preferred_auth_policies')
|
||||
if policies_str:
|
||||
if isinstance(policies_str, bytes):
|
||||
policies_str = str(policies_str, encoding="utf-8")
|
||||
for uri in policies_str.split(' '):
|
||||
if uri not in self.preferred_auth_policies:
|
||||
self.preferred_auth_policies.append(uri)
|
||||
|
||||
# max_auth_age is base-10 integer number of seconds
|
||||
max_auth_age_str = args.get('max_auth_age')
|
||||
self.max_auth_age = None
|
||||
|
||||
if max_auth_age_str:
|
||||
try:
|
||||
self.max_auth_age = int(max_auth_age_str)
|
||||
except ValueError:
|
||||
if strict:
|
||||
raise
|
||||
|
||||
# Parse auth level information
|
||||
preferred_auth_level_types = args.get('preferred_auth_level_types')
|
||||
if preferred_auth_level_types:
|
||||
aliases = preferred_auth_level_types.strip().split()
|
||||
|
||||
for alias in aliases:
|
||||
key = 'auth_level.ns.%s' % (alias, )
|
||||
try:
|
||||
uri = args[key]
|
||||
except KeyError:
|
||||
if is_openid1:
|
||||
uri = self._default_auth_level_aliases.get(alias)
|
||||
else:
|
||||
uri = None
|
||||
|
||||
if uri is None:
|
||||
if strict:
|
||||
raise ValueError('preferred auth level %r is not '
|
||||
'defined in this message' % (alias, ))
|
||||
else:
|
||||
self.addAuthLevel(uri, alias)
|
||||
|
||||
def preferredTypes(self, supported_types):
|
||||
"""Given a list of authentication policy URIs that a provider
|
||||
supports, this method returns the subsequence of those types
|
||||
that are preferred by the relying party.
|
||||
|
||||
@param supported_types: A sequence of authentication policy
|
||||
type URIs that are supported by a provider
|
||||
|
||||
@returns: The sub-sequence of the supported types that are
|
||||
preferred by the relying party. This list will be ordered
|
||||
in the order that the types appear in the supported_types
|
||||
sequence, and may be empty if the provider does not prefer
|
||||
any of the supported authentication types.
|
||||
|
||||
@returntype: [str]
|
||||
"""
|
||||
return list(
|
||||
filter(self.preferred_auth_policies.__contains__, supported_types))
|
||||
|
||||
|
||||
Request.ns_uri = ns_uri
|
||||
|
||||
|
||||
class Response(PAPEExtension):
|
||||
"""A Provider Authentication Policy response, sent from a provider
|
||||
to a relying party
|
||||
|
||||
@ivar auth_policies: List of authentication policies conformed to
|
||||
by this OpenID assertion, represented as policy URIs
|
||||
"""
|
||||
|
||||
ns_alias = 'pape'
|
||||
|
||||
def __init__(self, auth_policies=None, auth_time=None, auth_levels=None):
|
||||
super(Response, self).__init__()
|
||||
if auth_policies:
|
||||
self.auth_policies = auth_policies
|
||||
else:
|
||||
self.auth_policies = []
|
||||
|
||||
self.auth_time = auth_time
|
||||
self.auth_levels = {}
|
||||
|
||||
if auth_levels is None:
|
||||
auth_levels = {}
|
||||
|
||||
for uri, level in auth_levels.items():
|
||||
self.setAuthLevel(uri, level)
|
||||
|
||||
def setAuthLevel(self, level_uri, level, alias=None):
|
||||
"""Set the value for the given auth level type.
|
||||
|
||||
@param level: string representation of an authentication level
|
||||
valid for level_uri
|
||||
|
||||
@param alias: An optional namespace alias for the given auth
|
||||
level URI. May be omitted if the alias is not
|
||||
significant. The library will use a reasonable default for
|
||||
widely-used auth level types.
|
||||
"""
|
||||
self._addAuthLevelAlias(level_uri, alias)
|
||||
self.auth_levels[level_uri] = level
|
||||
|
||||
def getAuthLevel(self, level_uri):
|
||||
"""Return the auth level for the specified auth level
|
||||
identifier
|
||||
|
||||
@returns: A string that should map to the auth levels defined
|
||||
for the auth level type
|
||||
|
||||
@raises KeyError: If the auth level type is not present in
|
||||
this message
|
||||
"""
|
||||
return self.auth_levels[level_uri]
|
||||
|
||||
def _getNISTAuthLevel(self):
|
||||
try:
|
||||
return int(self.getAuthLevel(LEVELS_NIST))
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
nist_auth_level = property(
|
||||
_getNISTAuthLevel,
|
||||
doc="Backward-compatibility accessor for the NIST auth level")
|
||||
|
||||
def addPolicyURI(self, policy_uri):
|
||||
"""Add a authentication policy to this response
|
||||
|
||||
This method is intended to be used by the provider to add a
|
||||
policy that the provider conformed to when authenticating the user.
|
||||
|
||||
@param policy_uri: The identifier for the preferred type of
|
||||
authentication.
|
||||
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
|
||||
"""
|
||||
if policy_uri == AUTH_NONE:
|
||||
raise RuntimeError(
|
||||
'To send no policies, do not set any on the response.')
|
||||
|
||||
if policy_uri not in self.auth_policies:
|
||||
self.auth_policies.append(policy_uri)
|
||||
|
||||
def fromSuccessResponse(cls, success_response):
|
||||
"""Create a C{L{Response}} object from a successful OpenID
|
||||
library response
|
||||
(C{L{openid.consumer.consumer.SuccessResponse}}) response
|
||||
message
|
||||
|
||||
@param success_response: A SuccessResponse from consumer.complete()
|
||||
@type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
|
||||
|
||||
@rtype: Response or None
|
||||
@returns: A provider authentication policy response from the
|
||||
data that was supplied with the C{id_res} response or None
|
||||
if the provider sent no signed PAPE response arguments.
|
||||
"""
|
||||
self = cls()
|
||||
|
||||
# PAPE requires that the args be signed.
|
||||
args = success_response.getSignedNS(self.ns_uri)
|
||||
is_openid1 = success_response.isOpenID1()
|
||||
|
||||
# Only try to construct a PAPE response if the arguments were
|
||||
# signed in the OpenID response. If not, return None.
|
||||
if args is not None:
|
||||
self.parseExtensionArgs(args, is_openid1)
|
||||
return self
|
||||
else:
|
||||
return None
|
||||
|
||||
def parseExtensionArgs(self, args, is_openid1, strict=False):
|
||||
"""Parse the provider authentication policy arguments into the
|
||||
internal state of this object
|
||||
|
||||
@param args: unqualified provider authentication policy
|
||||
arguments
|
||||
|
||||
@param strict: Whether to raise an exception when bad data is
|
||||
encountered
|
||||
|
||||
@returns: None. The data is parsed into the internal fields of
|
||||
this object.
|
||||
"""
|
||||
policies_str = args.get('auth_policies')
|
||||
if policies_str:
|
||||
auth_policies = policies_str.split(' ')
|
||||
elif strict:
|
||||
raise ValueError('Missing auth_policies')
|
||||
else:
|
||||
auth_policies = []
|
||||
|
||||
if (len(auth_policies) > 1 and strict and AUTH_NONE in auth_policies):
|
||||
raise ValueError('Got some auth policies, as well as the special '
|
||||
'"none" URI: %r' % (auth_policies, ))
|
||||
|
||||
if 'none' in auth_policies:
|
||||
msg = '"none" used as a policy URI (see PAPE draft < 5)'
|
||||
if strict:
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
|
||||
auth_policies = [
|
||||
u for u in auth_policies if u not in ['none', AUTH_NONE]
|
||||
]
|
||||
|
||||
self.auth_policies = auth_policies
|
||||
|
||||
for (key, val) in args.items():
|
||||
if key.startswith('auth_level.'):
|
||||
alias = key[11:]
|
||||
|
||||
# skip the already-processed namespace declarations
|
||||
if alias.startswith('ns.'):
|
||||
continue
|
||||
|
||||
try:
|
||||
uri = args['auth_level.ns.%s' % (alias, )]
|
||||
except KeyError:
|
||||
if is_openid1:
|
||||
uri = self._default_auth_level_aliases.get(alias)
|
||||
else:
|
||||
uri = None
|
||||
|
||||
if uri is None:
|
||||
if strict:
|
||||
raise ValueError('Undefined auth level alias: %r' %
|
||||
(alias, ))
|
||||
else:
|
||||
self.setAuthLevel(uri, val, alias)
|
||||
|
||||
auth_time = args.get('auth_time')
|
||||
if auth_time:
|
||||
if TIME_VALIDATOR.match(auth_time):
|
||||
self.auth_time = auth_time
|
||||
elif strict:
|
||||
raise ValueError("auth_time must be in RFC3339 format")
|
||||
|
||||
fromSuccessResponse = classmethod(fromSuccessResponse)
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""@see: C{L{Extension.getExtensionArgs}}
|
||||
"""
|
||||
if len(self.auth_policies) == 0:
|
||||
ns_args = {
|
||||
'auth_policies': AUTH_NONE,
|
||||
}
|
||||
else:
|
||||
ns_args = {
|
||||
'auth_policies': ' '.join(self.auth_policies),
|
||||
}
|
||||
|
||||
for level_type, level in self.auth_levels.items():
|
||||
alias = self._getAlias(level_type)
|
||||
ns_args['auth_level.ns.%s' % (alias, )] = level_type
|
||||
ns_args['auth_level.%s' % (alias, )] = str(level)
|
||||
|
||||
if self.auth_time is not None:
|
||||
if not TIME_VALIDATOR.match(self.auth_time):
|
||||
raise ValueError('auth_time must be in RFC3339 format')
|
||||
|
||||
ns_args['auth_time'] = self.auth_time
|
||||
|
||||
return ns_args
|
||||
|
||||
|
||||
Response.ns_uri = ns_uri
|
||||
@@ -0,0 +1,527 @@
|
||||
"""Simple registration request and response parsing and object representation
|
||||
|
||||
This module contains objects representing simple registration requests
|
||||
and responses that can be used with both OpenID relying parties and
|
||||
OpenID providers.
|
||||
|
||||
1. The relying party creates a request object and adds it to the
|
||||
C{L{AuthRequest<openid.consumer.consumer.AuthRequest>}} object
|
||||
before making the C{checkid_} request to the OpenID provider::
|
||||
|
||||
auth_request.addExtension(SRegRequest(required=['email']))
|
||||
|
||||
2. The OpenID provider extracts the simple registration request from
|
||||
the OpenID request using C{L{SRegRequest.fromOpenIDRequest}},
|
||||
gets the user's approval and data, creates a C{L{SRegResponse}}
|
||||
object and adds it to the C{id_res} response::
|
||||
|
||||
sreg_req = SRegRequest.fromOpenIDRequest(checkid_request)
|
||||
# [ get the user's approval and data, informing the user that
|
||||
# the fields in sreg_response were requested ]
|
||||
sreg_resp = SRegResponse.extractResponse(sreg_req, user_data)
|
||||
sreg_resp.toMessage(openid_response.fields)
|
||||
|
||||
3. The relying party uses C{L{SRegResponse.fromSuccessResponse}} to
|
||||
extract the data from the OpenID response::
|
||||
|
||||
sreg_resp = SRegResponse.fromSuccessResponse(success_response)
|
||||
|
||||
@since: 2.0
|
||||
|
||||
@var sreg_data_fields: The names of the data fields that are listed in
|
||||
the sreg spec, and a description of them in English
|
||||
|
||||
@var sreg_uri: The preferred URI to use for the simple registration
|
||||
namespace and XRD Type value
|
||||
"""
|
||||
|
||||
from openid.message import registerNamespaceAlias, \
|
||||
NamespaceAliasRegistrationError
|
||||
from openid.extension import Extension
|
||||
import logging
|
||||
|
||||
try:
|
||||
str #pylint:disable-msg=W0104
|
||||
except NameError:
|
||||
# For Python 2.2
|
||||
str = (str, str) #pylint:disable-msg=W0622
|
||||
|
||||
__all__ = [
|
||||
'SRegRequest',
|
||||
'SRegResponse',
|
||||
'data_fields',
|
||||
'ns_uri',
|
||||
'ns_uri_1_0',
|
||||
'ns_uri_1_1',
|
||||
'supportsSReg',
|
||||
]
|
||||
|
||||
# The data fields that are listed in the sreg spec
|
||||
data_fields = {
|
||||
'fullname': 'Full Name',
|
||||
'nickname': 'Nickname',
|
||||
'dob': 'Date of Birth',
|
||||
'email': 'E-mail Address',
|
||||
'gender': 'Gender',
|
||||
'postcode': 'Postal Code',
|
||||
'country': 'Country',
|
||||
'language': 'Language',
|
||||
'timezone': 'Time Zone',
|
||||
}
|
||||
|
||||
|
||||
def checkFieldName(field_name):
|
||||
"""Check to see that the given value is a valid simple
|
||||
registration data field name.
|
||||
|
||||
@raise ValueError: if the field name is not a valid simple
|
||||
registration data field name
|
||||
"""
|
||||
if field_name not in data_fields:
|
||||
raise ValueError('%r is not a defined simple registration field' %
|
||||
(field_name, ))
|
||||
|
||||
|
||||
# URI used in the wild for Yadis documents advertising simple
|
||||
# registration support
|
||||
ns_uri_1_0 = 'http://openid.net/sreg/1.0'
|
||||
|
||||
# URI in the draft specification for simple registration 1.1
|
||||
# <http://openid.net/specs/openid-simple-registration-extension-1_1-01.html>
|
||||
ns_uri_1_1 = 'http://openid.net/extensions/sreg/1.1'
|
||||
|
||||
# This attribute will always hold the preferred URI to use when adding
|
||||
# sreg support to an XRDS file or in an OpenID namespace declaration.
|
||||
ns_uri = ns_uri_1_1
|
||||
|
||||
try:
|
||||
registerNamespaceAlias(ns_uri_1_1, 'sreg')
|
||||
except NamespaceAliasRegistrationError as e:
|
||||
logging.exception('registerNamespaceAlias(%r, %r) failed: %s' %
|
||||
(ns_uri_1_1, 'sreg', str(e), ))
|
||||
|
||||
|
||||
def supportsSReg(endpoint):
|
||||
"""Does the given endpoint advertise support for simple
|
||||
registration?
|
||||
|
||||
@param endpoint: The endpoint object as returned by OpenID discovery
|
||||
@type endpoint: openid.consumer.discover.OpenIDEndpoint
|
||||
|
||||
@returns: Whether an sreg type was advertised by the endpoint
|
||||
@rtype: bool
|
||||
"""
|
||||
return (endpoint.usesExtension(ns_uri_1_1) or
|
||||
endpoint.usesExtension(ns_uri_1_0))
|
||||
|
||||
|
||||
class SRegNamespaceError(ValueError):
|
||||
"""The simple registration namespace was not found and could not
|
||||
be created using the expected name (there's another extension
|
||||
using the name 'sreg')
|
||||
|
||||
This is not I{illegal}, for OpenID 2, although it probably
|
||||
indicates a problem, since it's not expected that other extensions
|
||||
will re-use the alias that is in use for OpenID 1.
|
||||
|
||||
If this is an OpenID 1 request, then there is no recourse. This
|
||||
should not happen unless some code has modified the namespaces for
|
||||
the message that is being processed.
|
||||
"""
|
||||
|
||||
|
||||
def getSRegNS(message):
|
||||
"""Extract the simple registration namespace URI from the given
|
||||
OpenID message. Handles OpenID 1 and 2, as well as both sreg
|
||||
namespace URIs found in the wild, as well as missing namespace
|
||||
definitions (for OpenID 1)
|
||||
|
||||
@param message: The OpenID message from which to parse simple
|
||||
registration fields. This may be a request or response message.
|
||||
@type message: C{L{openid.message.Message}}
|
||||
|
||||
@returns: the sreg namespace URI for the supplied message. The
|
||||
message may be modified to define a simple registration
|
||||
namespace.
|
||||
@rtype: C{str}
|
||||
|
||||
@raise ValueError: when using OpenID 1 if the message defines
|
||||
the 'sreg' alias to be something other than a simple
|
||||
registration type.
|
||||
"""
|
||||
# See if there exists an alias for one of the two defined simple
|
||||
# registration types.
|
||||
for sreg_ns_uri in [ns_uri_1_1, ns_uri_1_0]:
|
||||
alias = message.namespaces.getAlias(sreg_ns_uri)
|
||||
if alias is not None:
|
||||
break
|
||||
else:
|
||||
# There is no alias for either of the types, so try to add
|
||||
# one. We default to using the modern value (1.1)
|
||||
sreg_ns_uri = ns_uri_1_1
|
||||
try:
|
||||
message.namespaces.addAlias(ns_uri_1_1, 'sreg')
|
||||
except KeyError as why:
|
||||
# An alias for the string 'sreg' already exists, but it's
|
||||
# defined for something other than simple registration
|
||||
raise SRegNamespaceError(why)
|
||||
|
||||
# we know that sreg_ns_uri defined, because it's defined in the
|
||||
# else clause of the loop as well, so disable the warning
|
||||
return sreg_ns_uri #pylint:disable-msg=W0631
|
||||
|
||||
|
||||
class SRegRequest(Extension):
|
||||
"""An object to hold the state of a simple registration request.
|
||||
|
||||
@ivar required: A list of the required fields in this simple
|
||||
registration request
|
||||
@type required: [str]
|
||||
|
||||
@ivar optional: A list of the optional fields in this simple
|
||||
registration request
|
||||
@type optional: [str]
|
||||
|
||||
@ivar policy_url: The policy URL that was provided with the request
|
||||
@type policy_url: str or NoneType
|
||||
|
||||
@group Consumer: requestField, requestFields, getExtensionArgs, addToOpenIDRequest
|
||||
@group Server: fromOpenIDRequest, parseExtensionArgs
|
||||
"""
|
||||
|
||||
ns_alias = 'sreg'
|
||||
|
||||
def __init__(self,
|
||||
required=None,
|
||||
optional=None,
|
||||
policy_url=None,
|
||||
sreg_ns_uri=ns_uri):
|
||||
"""Initialize an empty simple registration request"""
|
||||
Extension.__init__(self)
|
||||
self.required = []
|
||||
self.optional = []
|
||||
self.policy_url = policy_url
|
||||
self.ns_uri = sreg_ns_uri
|
||||
|
||||
if required:
|
||||
self.requestFields(required, required=True, strict=True)
|
||||
|
||||
if optional:
|
||||
self.requestFields(optional, required=False, strict=True)
|
||||
|
||||
# Assign getSRegNS to a static method so that it can be
|
||||
# overridden for testing.
|
||||
_getSRegNS = staticmethod(getSRegNS)
|
||||
|
||||
def fromOpenIDRequest(cls, request):
|
||||
"""Create a simple registration request that contains the
|
||||
fields that were requested in the OpenID request with the
|
||||
given arguments
|
||||
|
||||
@param request: The OpenID request
|
||||
@type request: openid.server.CheckIDRequest
|
||||
|
||||
@returns: The newly created simple registration request
|
||||
@rtype: C{L{SRegRequest}}
|
||||
"""
|
||||
self = cls()
|
||||
|
||||
# Since we're going to mess with namespace URI mapping, don't
|
||||
# mutate the object that was passed in.
|
||||
message = request.message.copy()
|
||||
|
||||
self.ns_uri = self._getSRegNS(message)
|
||||
args = message.getArgs(self.ns_uri)
|
||||
self.parseExtensionArgs(args)
|
||||
|
||||
return self
|
||||
|
||||
fromOpenIDRequest = classmethod(fromOpenIDRequest)
|
||||
|
||||
def parseExtensionArgs(self, args, strict=False):
|
||||
"""Parse the unqualified simple registration request
|
||||
parameters and add them to this object.
|
||||
|
||||
This method is essentially the inverse of
|
||||
C{L{getExtensionArgs}}. This method restores the serialized simple
|
||||
registration request fields.
|
||||
|
||||
If you are extracting arguments from a standard OpenID
|
||||
checkid_* request, you probably want to use C{L{fromOpenIDRequest}},
|
||||
which will extract the sreg namespace and arguments from the
|
||||
OpenID request. This method is intended for cases where the
|
||||
OpenID server needs more control over how the arguments are
|
||||
parsed than that method provides.
|
||||
|
||||
>>> args = message.getArgs(ns_uri)
|
||||
>>> request.parseExtensionArgs(args)
|
||||
|
||||
@param args: The unqualified simple registration arguments
|
||||
@type args: {str:str}
|
||||
|
||||
@param strict: Whether requests with fields that are not
|
||||
defined in the simple registration specification should be
|
||||
tolerated (and ignored)
|
||||
@type strict: bool
|
||||
|
||||
@returns: None; updates this object
|
||||
"""
|
||||
for list_name in ['required', 'optional']:
|
||||
required = (list_name == 'required')
|
||||
items = args.get(list_name)
|
||||
if items:
|
||||
for field_name in items.split(','):
|
||||
try:
|
||||
self.requestField(field_name, required, strict)
|
||||
except ValueError:
|
||||
if strict:
|
||||
raise
|
||||
|
||||
self.policy_url = args.get('policy_url')
|
||||
|
||||
def allRequestedFields(self):
|
||||
"""A list of all of the simple registration fields that were
|
||||
requested, whether they were required or optional.
|
||||
|
||||
@rtype: [str]
|
||||
"""
|
||||
return self.required + self.optional
|
||||
|
||||
def wereFieldsRequested(self):
|
||||
"""Have any simple registration fields been requested?
|
||||
|
||||
@rtype: bool
|
||||
"""
|
||||
return bool(self.allRequestedFields())
|
||||
|
||||
def __contains__(self, field_name):
|
||||
"""Was this field in the request?"""
|
||||
return (field_name in self.required or field_name in self.optional)
|
||||
|
||||
def requestField(self, field_name, required=False, strict=False):
|
||||
"""Request the specified field from the OpenID user
|
||||
|
||||
@param field_name: the unqualified simple registration field name
|
||||
@type field_name: str
|
||||
|
||||
@param required: whether the given field should be presented
|
||||
to the user as being a required to successfully complete
|
||||
the request
|
||||
|
||||
@param strict: whether to raise an exception when a field is
|
||||
added to a request more than once
|
||||
|
||||
@raise ValueError: when the field requested is not a simple
|
||||
registration field or strict is set and the field was
|
||||
requested more than once
|
||||
"""
|
||||
checkFieldName(field_name)
|
||||
|
||||
if strict:
|
||||
if field_name in self.required or field_name in self.optional:
|
||||
raise ValueError('That field has already been requested')
|
||||
else:
|
||||
if field_name in self.required:
|
||||
return
|
||||
|
||||
if field_name in self.optional:
|
||||
if required:
|
||||
self.optional.remove(field_name)
|
||||
else:
|
||||
return
|
||||
|
||||
if required:
|
||||
self.required.append(field_name)
|
||||
else:
|
||||
self.optional.append(field_name)
|
||||
|
||||
def requestFields(self, field_names, required=False, strict=False):
|
||||
"""Add the given list of fields to the request
|
||||
|
||||
@param field_names: The simple registration data fields to request
|
||||
@type field_names: [str]
|
||||
|
||||
@param required: Whether these values should be presented to
|
||||
the user as required
|
||||
|
||||
@param strict: whether to raise an exception when a field is
|
||||
added to a request more than once
|
||||
|
||||
@raise ValueError: when a field requested is not a simple
|
||||
registration field or strict is set and a field was
|
||||
requested more than once
|
||||
"""
|
||||
if isinstance(field_names, str):
|
||||
raise TypeError('Fields should be passed as a list of '
|
||||
'strings (not %r)' % (type(field_names), ))
|
||||
|
||||
for field_name in field_names:
|
||||
self.requestField(field_name, required, strict=strict)
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""Get a dictionary of unqualified simple registration
|
||||
arguments representing this request.
|
||||
|
||||
This method is essentially the inverse of
|
||||
C{L{parseExtensionArgs}}. This method serializes the simple
|
||||
registration request fields.
|
||||
|
||||
@rtype: {str:str}
|
||||
"""
|
||||
args = {}
|
||||
|
||||
if self.required:
|
||||
args['required'] = ','.join(self.required)
|
||||
|
||||
if self.optional:
|
||||
args['optional'] = ','.join(self.optional)
|
||||
|
||||
if self.policy_url:
|
||||
args['policy_url'] = self.policy_url
|
||||
|
||||
return args
|
||||
|
||||
|
||||
class SRegResponse(Extension):
|
||||
"""Represents the data returned in a simple registration response
|
||||
inside of an OpenID C{id_res} response. This object will be
|
||||
created by the OpenID server, added to the C{id_res} response
|
||||
object, and then extracted from the C{id_res} message by the
|
||||
Consumer.
|
||||
|
||||
@ivar data: The simple registration data, keyed by the unqualified
|
||||
simple registration name of the field (i.e. nickname is keyed
|
||||
by C{'nickname'})
|
||||
|
||||
@ivar ns_uri: The URI under which the simple registration data was
|
||||
stored in the response message.
|
||||
|
||||
@group Server: extractResponse
|
||||
@group Consumer: fromSuccessResponse
|
||||
@group Read-only dictionary interface: keys, iterkeys, items, iteritems,
|
||||
__iter__, get, __getitem__, keys, has_key
|
||||
"""
|
||||
|
||||
ns_alias = 'sreg'
|
||||
|
||||
def __init__(self, data=None, sreg_ns_uri=ns_uri):
|
||||
Extension.__init__(self)
|
||||
if data is None:
|
||||
self.data = {}
|
||||
else:
|
||||
self.data = data
|
||||
|
||||
self.ns_uri = sreg_ns_uri
|
||||
|
||||
def extractResponse(cls, request, data):
|
||||
"""Take a C{L{SRegRequest}} and a dictionary of simple
|
||||
registration values and create a C{L{SRegResponse}}
|
||||
object containing that data.
|
||||
|
||||
@param request: The simple registration request object
|
||||
@type request: SRegRequest
|
||||
|
||||
@param data: The simple registration data for this
|
||||
response, as a dictionary from unqualified simple
|
||||
registration field name to string (unicode) value. For
|
||||
instance, the nickname should be stored under the key
|
||||
'nickname'.
|
||||
@type data: {str:str}
|
||||
|
||||
@returns: a simple registration response object
|
||||
@rtype: SRegResponse
|
||||
"""
|
||||
self = cls()
|
||||
self.ns_uri = request.ns_uri
|
||||
for field in request.allRequestedFields():
|
||||
value = data.get(field)
|
||||
if value is not None:
|
||||
self.data[field] = value
|
||||
return self
|
||||
|
||||
extractResponse = classmethod(extractResponse)
|
||||
|
||||
# Assign getSRegArgs to a static method so that it can be
|
||||
# overridden for testing
|
||||
_getSRegNS = staticmethod(getSRegNS)
|
||||
|
||||
def fromSuccessResponse(cls, success_response, signed_only=True):
|
||||
"""Create a C{L{SRegResponse}} object from a successful OpenID
|
||||
library response
|
||||
(C{L{openid.consumer.consumer.SuccessResponse}}) response
|
||||
message
|
||||
|
||||
@param success_response: A SuccessResponse from consumer.complete()
|
||||
@type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
|
||||
|
||||
@param signed_only: Whether to process only data that was
|
||||
signed in the id_res message from the server.
|
||||
@type signed_only: bool
|
||||
|
||||
@rtype: SRegResponse
|
||||
@returns: A simple registration response containing the data
|
||||
that was supplied with the C{id_res} response.
|
||||
"""
|
||||
self = cls()
|
||||
self.ns_uri = self._getSRegNS(success_response.message)
|
||||
if signed_only:
|
||||
args = success_response.getSignedNS(self.ns_uri)
|
||||
else:
|
||||
args = success_response.message.getArgs(self.ns_uri)
|
||||
|
||||
if not args:
|
||||
return None
|
||||
|
||||
for field_name in data_fields:
|
||||
if field_name in args:
|
||||
self.data[field_name] = args[field_name]
|
||||
|
||||
return self
|
||||
|
||||
fromSuccessResponse = classmethod(fromSuccessResponse)
|
||||
|
||||
def getExtensionArgs(self):
|
||||
"""Get the fields to put in the simple registration namespace
|
||||
when adding them to an id_res message.
|
||||
|
||||
@see: openid.extension
|
||||
"""
|
||||
return self.data
|
||||
|
||||
# Read-only dictionary interface
|
||||
def get(self, field_name, default=None):
|
||||
"""Like dict.get, except that it checks that the field name is
|
||||
defined by the simple registration specification"""
|
||||
checkFieldName(field_name)
|
||||
return self.data.get(field_name, default)
|
||||
|
||||
def items(self):
|
||||
"""All of the data values in this simple registration response
|
||||
"""
|
||||
return list(self.data.items())
|
||||
|
||||
def iteritems(self):
|
||||
return iter(self.data.items())
|
||||
|
||||
def keys(self):
|
||||
return list(self.data.keys())
|
||||
|
||||
def iterkeys(self):
|
||||
return iter(self.data.keys())
|
||||
|
||||
def has_key(self, key):
|
||||
return key in self
|
||||
|
||||
def __contains__(self, field_name):
|
||||
checkFieldName(field_name)
|
||||
return field_name in self.data
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
|
||||
def __getitem__(self, field_name):
|
||||
checkFieldName(field_name)
|
||||
return self.data[field_name]
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.data)
|
||||
Reference in New Issue
Block a user