1 """Extension argument processing code
3 __all__
= ['Message', 'NamespaceMap', 'no_default', 'registerNamespaceAlias',
4 'OPENID_NS', 'BARE_NS', 'OPENID1_NS', 'OPENID2_NS', 'SREG_URI',
11 from openid
import oidutil
12 from openid
import kvform
14 ElementTree
= oidutil
.importElementTree()
16 # No elementtree found, so give up, but don't fail to import,
17 # since we have fallbacks.
20 # This doesn't REALLY belong here, but where is better?
21 IDENTIFIER_SELECT
= 'http://specs.openid.net/auth/2.0/identifier_select'
23 # URI for Simple Registration extension, the only commonly deployed
24 # OpenID 1.x extension, and so a special case
25 SREG_URI
= 'http://openid.net/sreg/1.0'
27 # The OpenID 1.X namespace URI
28 OPENID1_NS
= 'http://openid.net/signon/1.0'
29 THE_OTHER_OPENID1_NS
= 'http://openid.net/signon/1.1'
31 OPENID1_NAMESPACES
= OPENID1_NS
, THE_OTHER_OPENID1_NS
33 # The OpenID 2.0 namespace URI
34 OPENID2_NS
= 'http://specs.openid.net/auth/2.0'
36 # The namespace consisting of pairs with keys that are prefixed with
37 # "openid." but not in another namespace.
38 NULL_NAMESPACE
= oidutil
.Symbol('Null namespace')
40 # The null namespace, when it is an allowed OpenID namespace
41 OPENID_NS
= oidutil
.Symbol('OpenID namespace')
43 # The top-level namespace, excluding all pairs with keys that start
45 BARE_NS
= oidutil
.Symbol('Bare namespace')
47 # Limit, in bytes, of identity provider and return_to URLs, including
48 # response payload. See OpenID 1.1 specification, Appendix D.
49 OPENID1_URL_LIMIT
= 2047
51 # All OpenID protocol fields. Used to check namespace aliases.
52 OPENID_PROTOCOL_FIELDS
= [
53 'ns', 'mode', 'error', 'return_to', 'contact', 'reference',
54 'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen',
55 'dh_consumer_public', 'claimed_id', 'identity', 'realm',
56 'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig',
57 'assoc_handle', 'trust_root', 'openid',
60 class UndefinedOpenIDNamespace(ValueError):
61 """Raised if the generic OpenID namespace is accessed when there
62 is no OpenID namespace set for this message."""
64 class InvalidOpenIDNamespace(ValueError):
65 """Raised if openid.ns is not a recognized value.
67 For recognized values, see L{Message.allowed_openid_namespaces}
70 s
= "Invalid OpenID Namespace"
72 s
+= " %r" % (self
.args
[0],)
76 # Sentinel used for Message implementation to indicate that getArg
77 # should raise an exception instead of returning a default.
80 # Global namespace / alias registration map. See
81 # registerNamespaceAlias.
82 registered_aliases
= {}
84 class NamespaceAliasRegistrationError(Exception):
86 Raised when an alias or namespace URI has already been registered.
90 def registerNamespaceAlias(namespace_uri
, alias
):
92 Registers a (namespace URI, alias) mapping in a global namespace
93 alias map. Raises NamespaceAliasRegistrationError if either the
94 namespace URI or alias has already been registered with a
95 different value. This function is required if you want to use a
96 namespace with an OpenID 1 message.
98 global registered_aliases
100 if registered_aliases
.get(alias
) == namespace_uri
:
103 if namespace_uri
in registered_aliases
.values():
104 raise NamespaceAliasRegistrationError
, \
105 'Namespace uri %r already registered' % (namespace_uri
,)
107 if alias
in registered_aliases
:
108 raise NamespaceAliasRegistrationError
, \
109 'Alias %r already registered' % (alias
,)
111 registered_aliases
[alias
] = namespace_uri
113 class Message(object):
115 In the implementation of this object, None represents the global
116 namespace as well as a namespace with no key.
118 @cvar namespaces: A dictionary specifying specific
119 namespace-URI to alias mappings that should be used when
120 generating namespace aliases.
122 @ivar ns_args: two-level dictionary of the values in this message,
123 grouped by namespace URI. The first level is the namespace
127 allowed_openid_namespaces
= [OPENID1_NS
, THE_OTHER_OPENID1_NS
, OPENID2_NS
]
129 def __init__(self
, openid_namespace
=None):
130 """Create an empty Message.
132 @raises InvalidOpenIDNamespace: if openid_namespace is not in
133 L{Message.allowed_openid_namespaces}
136 self
.namespaces
= NamespaceMap()
137 if openid_namespace
is None:
138 self
._openid
_ns
_uri
= None
140 implicit
= openid_namespace
in OPENID1_NAMESPACES
141 self
.setOpenIDNamespace(openid_namespace
, implicit
)
143 def fromPostArgs(cls
, args
):
144 """Construct a Message containing a set of POST arguments.
149 # Partition into "openid." args and bare args
151 for key
, value
in args
.items():
152 if isinstance(value
, list):
153 raise TypeError("query dict must have one value for each key, "
154 "not lists of values. Query is %r" % (args
,))
158 prefix
, rest
= key
.split('.', 1)
162 if prefix
!= 'openid':
163 self
.args
[(BARE_NS
, key
)] = value
165 openid_args
[rest
] = value
167 self
._fromOpenIDArgs
(openid_args
)
171 fromPostArgs
= classmethod(fromPostArgs
)
173 def fromOpenIDArgs(cls
, openid_args
):
174 """Construct a Message from a parsed KVForm message.
176 @raises InvalidOpenIDNamespace: if openid.ns is not in
177 L{Message.allowed_openid_namespaces}
180 self
._fromOpenIDArgs
(openid_args
)
183 fromOpenIDArgs
= classmethod(fromOpenIDArgs
)
185 def _fromOpenIDArgs(self
, openid_args
):
189 for rest
, value
in openid_args
.iteritems():
191 ns_alias
, ns_key
= rest
.split('.', 1)
193 ns_alias
= NULL_NAMESPACE
197 self
.namespaces
.addAlias(value
, ns_key
)
198 elif ns_alias
== NULL_NAMESPACE
and ns_key
== 'ns':
200 self
.setOpenIDNamespace(value
, False)
202 ns_args
.append((ns_alias
, ns_key
, value
))
204 # Implicitly set an OpenID namespace definition (OpenID 1)
205 if not self
.getOpenIDNamespace():
206 self
.setOpenIDNamespace(OPENID1_NS
, True)
208 # Actually put the pairs into the appropriate namespaces
209 for (ns_alias
, ns_key
, value
) in ns_args
:
210 ns_uri
= self
.namespaces
.getNamespaceURI(ns_alias
)
212 # we found a namespaced arg without a namespace URI defined
213 ns_uri
= self
._getDefaultNamespace
(ns_alias
)
215 ns_uri
= self
.getOpenIDNamespace()
216 ns_key
= '%s.%s' % (ns_alias
, ns_key
)
218 self
.namespaces
.addAlias(ns_uri
, ns_alias
, implicit
=True)
220 self
.setArg(ns_uri
, ns_key
, value
)
222 def _getDefaultNamespace(self
, mystery_alias
):
223 """OpenID 1 compatibility: look for a default namespace URI to
224 use for this alias."""
225 global registered_aliases
226 # Only try to map an alias to a default if it's an
227 # OpenID 1.x message.
229 return registered_aliases
.get(mystery_alias
)
233 def setOpenIDNamespace(self
, openid_ns_uri
, implicit
):
234 """Set the OpenID namespace URI used in this message.
236 @raises InvalidOpenIDNamespace: if the namespace is not in
237 L{Message.allowed_openid_namespaces}
239 if openid_ns_uri
not in self
.allowed_openid_namespaces
:
240 raise InvalidOpenIDNamespace(openid_ns_uri
)
242 self
.namespaces
.addAlias(openid_ns_uri
, NULL_NAMESPACE
, implicit
)
243 self
._openid
_ns
_uri
= openid_ns_uri
245 def getOpenIDNamespace(self
):
246 return self
._openid
_ns
_uri
249 return self
.getOpenIDNamespace() in OPENID1_NAMESPACES
252 return self
.getOpenIDNamespace() == OPENID2_NS
254 def fromKVForm(cls
, kvform_string
):
255 """Create a Message from a KVForm string"""
256 return cls
.fromOpenIDArgs(kvform
.kvToDict(kvform_string
))
258 fromKVForm
= classmethod(fromKVForm
)
261 return copy
.deepcopy(self
)
263 def toPostArgs(self
):
264 """Return all arguments with openid. in front of namespaced arguments.
268 # Add namespace definitions to the output
269 for ns_uri
, alias
in self
.namespaces
.iteritems():
270 if self
.namespaces
.isImplicit(ns_uri
):
272 if alias
== NULL_NAMESPACE
:
275 ns_key
= 'openid.ns.' + alias
276 args
[ns_key
] = ns_uri
278 for (ns_uri
, ns_key
), value
in self
.args
.iteritems():
279 key
= self
.getKey(ns_uri
, ns_key
)
280 args
[key
] = value
.encode('UTF-8')
285 """Return all namespaced arguments, failing if any
286 non-namespaced arguments exist."""
287 # FIXME - undocumented exception
288 post_args
= self
.toPostArgs()
290 for k
, v
in post_args
.iteritems():
291 if not k
.startswith('openid.'):
293 'This message can only be encoded as a POST, because it '
294 'contains arguments that are not prefixed with "openid."')
300 def toFormMarkup(self
, action_url
, form_tag_attrs
=None,
301 submit_text
="Continue"):
302 """Generate HTML form markup that contains the values in this
303 message, to be HTTP POSTed as x-www-form-urlencoded UTF-8.
305 @param action_url: The URL to which the form will be POSTed
306 @type action_url: str
308 @param form_tag_attrs: Dictionary of attributes to be added to
309 the form tag. 'accept-charset' and 'enctype' have defaults
310 that can be overridden. If a value is supplied for
311 'action' or 'method', it will be replaced.
312 @type form_tag_attrs: {unicode: unicode}
314 @param submit_text: The text that will appear on the submit
315 button for this form.
316 @type submit_text: unicode
318 @returns: A string containing (X)HTML markup for a form that
319 encodes the values in this Message object.
320 @rtype: str or unicode
322 if ElementTree
is None:
323 raise RuntimeError('This function requires ElementTree.')
325 assert action_url
is not None
327 form
= ElementTree
.Element('form')
330 for name
, attr
in form_tag_attrs
.iteritems():
331 form
.attrib
[name
] = attr
333 form
.attrib
['action'] = action_url
334 form
.attrib
['method'] = 'post'
335 form
.attrib
['accept-charset'] = 'UTF-8'
336 form
.attrib
['enctype'] = 'application/x-www-form-urlencoded'
338 for name
, value
in self
.toPostArgs().iteritems():
339 attrs
= {'type': 'hidden',
342 form
.append(ElementTree
.Element('input', attrs
))
344 submit
= ElementTree
.Element(
345 'input', {'type':'submit', 'value':submit_text
})
348 return ElementTree
.tostring(form
)
350 def toURL(self
, base_url
):
351 """Generate a GET URL with the parameters in this message
352 attached as query parameters."""
353 return oidutil
.appendArgs(base_url
, self
.toPostArgs())
356 """Generate a KVForm string that contains the parameters in
357 this message. This will fail if the message contains arguments
358 outside of the 'openid.' prefix.
360 return kvform
.dictToKV(self
.toArgs())
362 def toURLEncoded(self
):
363 """Generate an x-www-urlencoded string"""
364 args
= self
.toPostArgs().items()
366 return urllib
.urlencode(args
)
368 def _fixNS(self
, namespace
):
369 """Convert an input value into the internally used values of
372 @param namespace: The string or constant to convert
373 @type namespace: str or unicode or BARE_NS or OPENID_NS
375 if namespace
== OPENID_NS
:
376 if self
._openid
_ns
_uri
is None:
377 raise UndefinedOpenIDNamespace('OpenID namespace not set')
379 namespace
= self
._openid
_ns
_uri
381 if namespace
!= BARE_NS
and type(namespace
) not in [str, unicode]:
383 "Namespace must be BARE_NS, OPENID_NS or a string. got %r"
386 if namespace
!= BARE_NS
and ':' not in namespace
:
387 fmt
= 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r'
388 warnings
.warn(fmt
% (namespace
,), DeprecationWarning)
390 if namespace
== 'sreg':
391 fmt
= 'Using %r instead of "sreg" as namespace'
392 warnings
.warn(fmt
% (SREG_URI
,), DeprecationWarning,)
397 def hasKey(self
, namespace
, ns_key
):
398 namespace
= self
._fixNS
(namespace
)
399 return (namespace
, ns_key
) in self
.args
401 def getKey(self
, namespace
, ns_key
):
402 """Get the key for a particular namespaced argument"""
403 namespace
= self
._fixNS
(namespace
)
404 if namespace
== BARE_NS
:
407 ns_alias
= self
.namespaces
.getAlias(namespace
)
409 # No alias is defined, so no key can exist
413 if ns_alias
== NULL_NAMESPACE
:
416 tail
= '%s.%s' % (ns_alias
, ns_key
)
418 return 'openid.' + tail
420 def getArg(self
, namespace
, key
, default
=None):
421 """Get a value for a namespaced key.
423 @param namespace: The namespace in the message for this key
426 @param key: The key to get within this namespace
429 @param default: The value to use if this key is absent from
430 this message. Using the special value
431 openid.message.no_default will result in this method
432 raising a KeyError instead of returning the default.
434 @rtype: str or the type of default
435 @raises KeyError: if default is no_default
436 @raises UndefinedOpenIDNamespace: if the message has not yet
437 had an OpenID namespace set
439 namespace
= self
._fixNS
(namespace
)
440 args_key
= (namespace
, key
)
442 return self
.args
[args_key
]
444 if default
is no_default
:
445 raise KeyError((namespace
, key
))
449 def getArgs(self
, namespace
):
450 """Get the arguments that are defined for this namespace URI
452 @returns: mapping from namespaced keys to values
455 namespace
= self
._fixNS
(namespace
)
458 for ((pair_ns
, ns_key
), value
)
459 in self
.args
.iteritems()
460 if pair_ns
== namespace
463 def updateArgs(self
, namespace
, updates
):
464 """Set multiple key/value pairs in one call
466 @param updates: The values to set
467 @type updates: {unicode:unicode}
469 namespace
= self
._fixNS
(namespace
)
470 for k
, v
in updates
.iteritems():
471 self
.setArg(namespace
, k
, v
)
473 def setArg(self
, namespace
, key
, value
):
474 """Set a single argument in this namespace"""
475 assert key
is not None
476 assert value
is not None
477 namespace
= self
._fixNS
(namespace
)
478 self
.args
[(namespace
, key
)] = value
479 if not (namespace
is BARE_NS
):
480 self
.namespaces
.add(namespace
)
482 def delArg(self
, namespace
, key
):
483 namespace
= self
._fixNS
(namespace
)
484 del self
.args
[(namespace
, key
)]
487 return "<%s.%s %r>" % (self
.__class
__.__module
__,
488 self
.__class
__.__name
__,
491 def __eq__(self
, other
):
492 return self
.args
== other
.args
495 def __ne__(self
, other
):
496 return not (self
== other
)
499 def getAliasedArg(self
, aliased_key
, default
=None):
500 if aliased_key
== 'ns':
501 return self
.getOpenIDNamespace()
503 if aliased_key
.startswith('ns.'):
504 uri
= self
.namespaces
.getNamespaceURI(aliased_key
[3:])
506 if default
== no_default
:
514 alias
, key
= aliased_key
.split('.', 1)
516 # need more than x values to unpack
519 ns
= self
.namespaces
.getNamespaceURI(alias
)
523 ns
= self
.getOpenIDNamespace()
525 return self
.getArg(ns
, key
, default
)
527 class NamespaceMap(object):
528 """Maintains a bijective map between namespace uris and aliases.
531 self
.alias_to_namespace
= {}
532 self
.namespace_to_alias
= {}
533 self
.implicit_namespaces
= []
535 def getAlias(self
, namespace_uri
):
536 return self
.namespace_to_alias
.get(namespace_uri
)
538 def getNamespaceURI(self
, alias
):
539 return self
.alias_to_namespace
.get(alias
)
541 def iterNamespaceURIs(self
):
542 """Return an iterator over the namespace URIs"""
543 return iter(self
.namespace_to_alias
)
545 def iterAliases(self
):
546 """Return an iterator over the aliases"""
547 return iter(self
.alias_to_namespace
)
550 """Iterate over the mapping
552 @returns: iterator of (namespace_uri, alias)
554 return self
.namespace_to_alias
.iteritems()
556 def addAlias(self
, namespace_uri
, desired_alias
, implicit
=False):
557 """Add an alias from this namespace URI to the desired alias
559 # Check that desired_alias is not an openid protocol field as
561 assert desired_alias
not in OPENID_PROTOCOL_FIELDS
, \
562 "%r is not an allowed namespace alias" % (desired_alias
,)
564 # Check that desired_alias does not contain a period as per
566 if type(desired_alias
) in [str, unicode]:
567 assert '.' not in desired_alias
, \
568 "%r must not contain a dot" % (desired_alias
,)
570 # Check that there is not a namespace already defined for
572 current_namespace_uri
= self
.alias_to_namespace
.get(desired_alias
)
573 if (current_namespace_uri
is not None
574 and current_namespace_uri
!= namespace_uri
):
576 fmt
= ('Cannot map %r to alias %r. '
577 '%r is already mapped to alias %r')
582 current_namespace_uri
,
586 # Check that there is not already a (different) alias for
588 alias
= self
.namespace_to_alias
.get(namespace_uri
)
589 if alias
is not None and alias
!= desired_alias
:
590 fmt
= ('Cannot map %r to alias %r. '
591 'It is already mapped to alias %r')
592 raise KeyError(fmt
% (namespace_uri
, desired_alias
, alias
))
594 assert (desired_alias
== NULL_NAMESPACE
or
595 type(desired_alias
) in [str, unicode]), repr(desired_alias
)
596 assert namespace_uri
not in self
.implicit_namespaces
597 self
.alias_to_namespace
[desired_alias
] = namespace_uri
598 self
.namespace_to_alias
[namespace_uri
] = desired_alias
600 self
.implicit_namespaces
.append(namespace_uri
)
603 def add(self
, namespace_uri
):
604 """Add this namespace URI to the mapping, without caring what
605 alias it ends up with"""
606 # See if this namespace is already mapped to an alias
607 alias
= self
.namespace_to_alias
.get(namespace_uri
)
608 if alias
is not None:
611 # Fall back to generating a numerical alias
614 alias
= 'ext' + str(i
)
616 self
.addAlias(namespace_uri
, alias
)
622 assert False, "Not reached"
624 def isDefined(self
, namespace_uri
):
625 return namespace_uri
in self
.namespace_to_alias
627 def __contains__(self
, namespace_uri
):
628 return self
.isDefined(namespace_uri
)
630 def isImplicit(self
, namespace_uri
):
631 return namespace_uri
in self
.implicit_namespaces