Package openid :: Module message
[frames] | no frames]

Source Code for Module openid.message

  1  """Extension argument processing code 
  2  """ 
  3  __all__ = ['Message', 'NamespaceMap', 'no_default', 'registerNamespaceAlias', 
  4             'OPENID_NS', 'BARE_NS', 'OPENID1_NS', 'OPENID2_NS', 'SREG_URI', 
  5             'IDENTIFIER_SELECT'] 
  6   
  7  import copy 
  8  import warnings 
  9  import urllib 
 10   
 11  from openid import oidutil 
 12  from openid import kvform 
 13  try: 
 14      ElementTree = oidutil.importElementTree() 
 15  except ImportError: 
 16      # No elementtree found, so give up, but don't fail to import, 
 17      # since we have fallbacks. 
 18      ElementTree = None 
 19   
 20  # This doesn't REALLY belong here, but where is better? 
 21  IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select' 
 22   
 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' 
 26   
 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' 
 30   
 31  OPENID1_NAMESPACES = OPENID1_NS, THE_OTHER_OPENID1_NS 
 32   
 33  # The OpenID 2.0 namespace URI 
 34  OPENID2_NS = 'http://specs.openid.net/auth/2.0' 
 35   
 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') 
 39   
 40  # The null namespace, when it is an allowed OpenID namespace 
 41  OPENID_NS = oidutil.Symbol('OpenID namespace') 
 42   
 43  # The top-level namespace, excluding all pairs with keys that start 
 44  # with "openid." 
 45  BARE_NS = oidutil.Symbol('Bare namespace') 
 46   
 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 
 50   
 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', 
 58      ] 
 59   
60 -class UndefinedOpenIDNamespace(ValueError):
61 """Raised if the generic OpenID namespace is accessed when there 62 is no OpenID namespace set for this message."""
63
64 -class InvalidOpenIDNamespace(ValueError):
65 """Raised if openid.ns is not a recognized value. 66 67 For recognized values, see L{Message.allowed_openid_namespaces} 68 """
69 - def __str__(self):
70 s = "Invalid OpenID Namespace" 71 if self.args: 72 s += " %r" % (self.args[0],) 73 return s
74 75 76 # Sentinel used for Message implementation to indicate that getArg 77 # should raise an exception instead of returning a default. 78 no_default = object() 79 80 # Global namespace / alias registration map. See 81 # registerNamespaceAlias. 82 registered_aliases = {} 83
84 -class NamespaceAliasRegistrationError(Exception):
85 """ 86 Raised when an alias or namespace URI has already been registered. 87 """ 88 pass
89
90 -def registerNamespaceAlias(namespace_uri, alias):
91 """ 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. 97 """ 98 global registered_aliases 99 100 if registered_aliases.get(alias) == namespace_uri: 101 return 102 103 if namespace_uri in registered_aliases.values(): 104 raise NamespaceAliasRegistrationError, \ 105 'Namespace uri %r already registered' % (namespace_uri,) 106 107 if alias in registered_aliases: 108 raise NamespaceAliasRegistrationError, \ 109 'Alias %r already registered' % (alias,) 110 111 registered_aliases[alias] = namespace_uri
112
113 -class Message(object):
114 """ 115 In the implementation of this object, None represents the global 116 namespace as well as a namespace with no key. 117 118 @cvar namespaces: A dictionary specifying specific 119 namespace-URI to alias mappings that should be used when 120 generating namespace aliases. 121 122 @ivar ns_args: two-level dictionary of the values in this message, 123 grouped by namespace URI. The first level is the namespace 124 URI. 125 """ 126 127 allowed_openid_namespaces = [OPENID1_NS, THE_OTHER_OPENID1_NS, OPENID2_NS] 128
129 - def __init__(self, openid_namespace=None):
130 """Create an empty Message. 131 132 @raises InvalidOpenIDNamespace: if openid_namespace is not in 133 L{Message.allowed_openid_namespaces} 134 """ 135 self.args = {} 136 self.namespaces = NamespaceMap() 137 if openid_namespace is None: 138 self._openid_ns_uri = None 139 else: 140 implicit = openid_namespace in OPENID1_NAMESPACES 141 self.setOpenIDNamespace(openid_namespace, implicit)
142
143 - def fromPostArgs(cls, args):
144 """Construct a Message containing a set of POST arguments. 145 146 """ 147 self = cls() 148 149 # Partition into "openid." args and bare args 150 openid_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,)) 155 156 157 try: 158 prefix, rest = key.split('.', 1) 159 except ValueError: 160 prefix = None 161 162 if prefix != 'openid': 163 self.args[(BARE_NS, key)] = value 164 else: 165 openid_args[rest] = value 166 167 self._fromOpenIDArgs(openid_args) 168 169 return self
170 171 fromPostArgs = classmethod(fromPostArgs) 172
173 - def fromOpenIDArgs(cls, openid_args):
174 """Construct a Message from a parsed KVForm message. 175 176 @raises InvalidOpenIDNamespace: if openid.ns is not in 177 L{Message.allowed_openid_namespaces} 178 """ 179 self = cls() 180 self._fromOpenIDArgs(openid_args) 181 return self
182 183 fromOpenIDArgs = classmethod(fromOpenIDArgs) 184
185 - def _fromOpenIDArgs(self, openid_args):
186 ns_args = [] 187 188 # Resolve namespaces 189 for rest, value in openid_args.iteritems(): 190 try: 191 ns_alias, ns_key = rest.split('.', 1) 192 except ValueError: 193 ns_alias = NULL_NAMESPACE 194 ns_key = rest 195 196 if ns_alias == 'ns': 197 self.namespaces.addAlias(value, ns_key) 198 elif ns_alias == NULL_NAMESPACE and ns_key == 'ns': 199 # null namespace 200 self.setOpenIDNamespace(value, False) 201 else: 202 ns_args.append((ns_alias, ns_key, value)) 203 204 # Implicitly set an OpenID namespace definition (OpenID 1) 205 if not self.getOpenIDNamespace(): 206 self.setOpenIDNamespace(OPENID1_NS, True) 207 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) 211 if ns_uri is None: 212 # we found a namespaced arg without a namespace URI defined 213 ns_uri = self._getDefaultNamespace(ns_alias) 214 if ns_uri is None: 215 ns_uri = self.getOpenIDNamespace() 216 ns_key = '%s.%s' % (ns_alias, ns_key) 217 else: 218 self.namespaces.addAlias(ns_uri, ns_alias, implicit=True) 219 220 self.setArg(ns_uri, ns_key, value)
221
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. 228 if self.isOpenID1(): 229 return registered_aliases.get(mystery_alias) 230 else: 231 return None
232
233 - def setOpenIDNamespace(self, openid_ns_uri, implicit):
234 """Set the OpenID namespace URI used in this message. 235 236 @raises InvalidOpenIDNamespace: if the namespace is not in 237 L{Message.allowed_openid_namespaces} 238 """ 239 if openid_ns_uri not in self.allowed_openid_namespaces: 240 raise InvalidOpenIDNamespace(openid_ns_uri) 241 242 self.namespaces.addAlias(openid_ns_uri, NULL_NAMESPACE, implicit) 243 self._openid_ns_uri = openid_ns_uri
244
245 - def getOpenIDNamespace(self):
246 return self._openid_ns_uri
247
248 - def isOpenID1(self):
250
251 - def isOpenID2(self):
252 return self.getOpenIDNamespace() == OPENID2_NS
253
254 - def fromKVForm(cls, kvform_string):
255 """Create a Message from a KVForm string""" 256 return cls.fromOpenIDArgs(kvform.kvToDict(kvform_string))
257 258 fromKVForm = classmethod(fromKVForm) 259
260 - def copy(self):
261 return copy.deepcopy(self)
262
263 - def toPostArgs(self):
264 """Return all arguments with openid. in front of namespaced arguments. 265 """ 266 args = {} 267 268 # Add namespace definitions to the output 269 for ns_uri, alias in self.namespaces.iteritems(): 270 if self.namespaces.isImplicit(ns_uri): 271 continue 272 if alias == NULL_NAMESPACE: 273 ns_key = 'openid.ns' 274 else: 275 ns_key = 'openid.ns.' + alias 276 args[ns_key] = ns_uri 277 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') 281 282 return args
283
284 - def toArgs(self):
285 """Return all namespaced arguments, failing if any 286 non-namespaced arguments exist.""" 287 # FIXME - undocumented exception 288 post_args = self.toPostArgs() 289 kvargs = {} 290 for k, v in post_args.iteritems(): 291 if not k.startswith('openid.'): 292 raise ValueError( 293 'This message can only be encoded as a POST, because it ' 294 'contains arguments that are not prefixed with "openid."') 295 else: 296 kvargs[k[7:]] = v 297 298 return kvargs
299
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. 304 305 @param action_url: The URL to which the form will be POSTed 306 @type action_url: str 307 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} 313 314 @param submit_text: The text that will appear on the submit 315 button for this form. 316 @type submit_text: unicode 317 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 321 """ 322 if ElementTree is None: 323 raise RuntimeError('This function requires ElementTree.') 324 325 assert action_url is not None 326 327 form = ElementTree.Element('form') 328 329 if form_tag_attrs: 330 for name, attr in form_tag_attrs.iteritems(): 331 form.attrib[name] = attr 332 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' 337 338 for name, value in self.toPostArgs().iteritems(): 339 attrs = {'type': 'hidden', 340 'name': name, 341 'value': value} 342 form.append(ElementTree.Element('input', attrs)) 343 344 submit = ElementTree.Element( 345 'input', {'type':'submit', 'value':submit_text}) 346 form.append(submit) 347 348 return ElementTree.tostring(form)
349
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())
354
355 - def toKVForm(self):
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. 359 """ 360 return kvform.dictToKV(self.toArgs())
361
362 - def toURLEncoded(self):
363 """Generate an x-www-urlencoded string""" 364 args = self.toPostArgs().items() 365 args.sort() 366 return urllib.urlencode(args)
367
368 - def _fixNS(self, namespace):
369 """Convert an input value into the internally used values of 370 this object 371 372 @param namespace: The string or constant to convert 373 @type namespace: str or unicode or BARE_NS or OPENID_NS 374 """ 375 if namespace == OPENID_NS: 376 if self._openid_ns_uri is None: 377 raise UndefinedOpenIDNamespace('OpenID namespace not set') 378 else: 379 namespace = self._openid_ns_uri 380 381 if namespace != BARE_NS and type(namespace) not in [str, unicode]: 382 raise TypeError( 383 "Namespace must be BARE_NS, OPENID_NS or a string. got %r" 384 % (namespace,)) 385 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) 389 390 if namespace == 'sreg': 391 fmt = 'Using %r instead of "sreg" as namespace' 392 warnings.warn(fmt % (SREG_URI,), DeprecationWarning,) 393 return SREG_URI 394 395 return namespace
396
397 - def hasKey(self, namespace, ns_key):
398 namespace = self._fixNS(namespace) 399 return (namespace, ns_key) in self.args
400
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: 405 return ns_key 406 407 ns_alias = self.namespaces.getAlias(namespace) 408 409 # No alias is defined, so no key can exist 410 if ns_alias is None: 411 return None 412 413 if ns_alias == NULL_NAMESPACE: 414 tail = ns_key 415 else: 416 tail = '%s.%s' % (ns_alias, ns_key) 417 418 return 'openid.' + tail
419
420 - def getArg(self, namespace, key, default=None):
421 """Get a value for a namespaced key. 422 423 @param namespace: The namespace in the message for this key 424 @type namespace: str 425 426 @param key: The key to get within this namespace 427 @type key: str 428 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. 433 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 438 """ 439 namespace = self._fixNS(namespace) 440 args_key = (namespace, key) 441 try: 442 return self.args[args_key] 443 except KeyError: 444 if default is no_default: 445 raise KeyError((namespace, key)) 446 else: 447 return default
448
449 - def getArgs(self, namespace):
450 """Get the arguments that are defined for this namespace URI 451 452 @returns: mapping from namespaced keys to values 453 @returntype: dict 454 """ 455 namespace = self._fixNS(namespace) 456 return dict([ 457 (ns_key, value) 458 for ((pair_ns, ns_key), value) 459 in self.args.iteritems() 460 if pair_ns == namespace 461 ])
462
463 - def updateArgs(self, namespace, updates):
464 """Set multiple key/value pairs in one call 465 466 @param updates: The values to set 467 @type updates: {unicode:unicode} 468 """ 469 namespace = self._fixNS(namespace) 470 for k, v in updates.iteritems(): 471 self.setArg(namespace, k, v)
472
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)
481
482 - def delArg(self, namespace, key):
483 namespace = self._fixNS(namespace) 484 del self.args[(namespace, key)]
485
486 - def __repr__(self):
487 return "<%s.%s %r>" % (self.__class__.__module__, 488 self.__class__.__name__, 489 self.args)
490
491 - def __eq__(self, other):
492 return self.args == other.args
493 494
495 - def __ne__(self, other):
496 return not (self == other)
497 498
499 - def getAliasedArg(self, aliased_key, default=None):
500 if aliased_key == 'ns': 501 return self.getOpenIDNamespace() 502 503 if aliased_key.startswith('ns.'): 504 uri = self.namespaces.getNamespaceURI(aliased_key[3:]) 505 if uri is None: 506 if default == no_default: 507 raise KeyError 508 else: 509 return default 510 else: 511 return uri 512 513 try: 514 alias, key = aliased_key.split('.', 1) 515 except ValueError: 516 # need more than x values to unpack 517 ns = None 518 else: 519 ns = self.namespaces.getNamespaceURI(alias) 520 521 if ns is None: 522 key = aliased_key 523 ns = self.getOpenIDNamespace() 524 525 return self.getArg(ns, key, default)
526
527 -class NamespaceMap(object):
528 """Maintains a bijective map between namespace uris and aliases. 529 """
530 - def __init__(self):
531 self.alias_to_namespace = {} 532 self.namespace_to_alias = {} 533 self.implicit_namespaces = []
534
535 - def getAlias(self, namespace_uri):
536 return self.namespace_to_alias.get(namespace_uri)
537
538 - def getNamespaceURI(self, alias):
539 return self.alias_to_namespace.get(alias)
540
541 - def iterNamespaceURIs(self):
542 """Return an iterator over the namespace URIs""" 543 return iter(self.namespace_to_alias)
544
545 - def iterAliases(self):
546 """Return an iterator over the aliases""" 547 return iter(self.alias_to_namespace)
548
549 - def iteritems(self):
550 """Iterate over the mapping 551 552 @returns: iterator of (namespace_uri, alias) 553 """ 554 return self.namespace_to_alias.iteritems()
555
556 - def addAlias(self, namespace_uri, desired_alias, implicit=False):
557 """Add an alias from this namespace URI to the desired alias 558 """ 559 # Check that desired_alias is not an openid protocol field as 560 # per the spec. 561 assert desired_alias not in OPENID_PROTOCOL_FIELDS, \ 562 "%r is not an allowed namespace alias" % (desired_alias,) 563 564 # Check that desired_alias does not contain a period as per 565 # the spec. 566 if type(desired_alias) in [str, unicode]: 567 assert '.' not in desired_alias, \ 568 "%r must not contain a dot" % (desired_alias,) 569 570 # Check that there is not a namespace already defined for 571 # the desired alias 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): 575 576 fmt = ('Cannot map %r to alias %r. ' 577 '%r is already mapped to alias %r') 578 579 msg = fmt % ( 580 namespace_uri, 581 desired_alias, 582 current_namespace_uri, 583 desired_alias) 584 raise KeyError(msg) 585 586 # Check that there is not already a (different) alias for 587 # this namespace URI 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)) 593 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 599 if implicit: 600 self.implicit_namespaces.append(namespace_uri) 601 return desired_alias
602
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: 609 return alias 610 611 # Fall back to generating a numerical alias 612 i = 0 613 while True: 614 alias = 'ext' + str(i) 615 try: 616 self.addAlias(namespace_uri, alias) 617 except KeyError: 618 i += 1 619 else: 620 return alias 621 622 assert False, "Not reached"
623
624 - def isDefined(self, namespace_uri):
625 return namespace_uri in self.namespace_to_alias
626
627 - def __contains__(self, namespace_uri):
628 return self.isDefined(namespace_uri)
629
630 - def isImplicit(self, namespace_uri):
631 return namespace_uri in self.implicit_namespaces
632