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', 
  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   
 30  # The OpenID 2.0 namespace URI 
 31  OPENID2_NS = 'http://specs.openid.net/auth/2.0' 
 32   
 33  # The namespace consisting of pairs with keys that are prefixed with 
 34  # "openid."  but not in another namespace. 
 35  NULL_NAMESPACE = oidutil.Symbol('Null namespace') 
 36   
 37  # The null namespace, when it is an allowed OpenID namespace 
 38  OPENID_NS = oidutil.Symbol('OpenID namespace') 
 39   
 40  # The top-level namespace, excluding all pairs with keys that start 
 41  # with "openid." 
 42  BARE_NS = oidutil.Symbol('Bare namespace') 
 43   
 44  # All OpenID protocol fields.  Used to check namespace aliases. 
 45  OPENID_PROTOCOL_FIELDS = [ 
 46      'ns', 'mode', 'error', 'return_to', 'contact', 'reference', 
 47      'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen', 
 48      'dh_consumer_public', 'claimed_id', 'identity', 'realm', 
 49      'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig', 
 50      'assoc_handle', 'trust_root', 'openid', 
 51      ] 
 52   
53 -class UndefinedOpenIDNamespace(ValueError):
54 """Raised if the generic OpenID namespace is accessed when there 55 is no OpenID namespace set for this message."""
56 57 # Sentinel used for Message implementation to indicate that getArg 58 # should raise an exception instead of returning a default. 59 no_default = object() 60 61 # Global namespace / alias registration map. See 62 # registerNamespaceAlias. 63 registered_aliases = {} 64
65 -class NamespaceAliasRegistrationError(Exception):
66 """ 67 Raised when an alias or namespace URI has already been registered. 68 """ 69 pass
70
71 -def registerNamespaceAlias(namespace_uri, alias):
72 """ 73 Registers a (namespace URI, alias) mapping in a global namespace 74 alias map. Raises NamespaceAliasRegistrationError if either the 75 namespace URI or alias has already been registered with a 76 different value. This function is required if you want to use a 77 namespace with an OpenID 1 message. 78 """ 79 global registered_aliases 80 81 if registered_aliases.get(alias) == namespace_uri: 82 return 83 84 if namespace_uri in registered_aliases.values(): 85 raise NamespaceAliasRegistrationError, \ 86 'Namespace uri %r already registered' % (namespace_uri,) 87 88 if alias in registered_aliases: 89 raise NamespaceAliasRegistrationError, \ 90 'Alias %r already registered' % (alias,) 91 92 registered_aliases[alias] = namespace_uri
93
94 -class Message(object):
95 """ 96 In the implementation of this object, None represents the global 97 namespace as well as a namespace with no key. 98 99 @cvar namespaces: A dictionary specifying specific 100 namespace-URI to alias mappings that should be used when 101 generating namespace aliases. 102 103 @ivar ns_args: two-level dictionary of the values in this message, 104 grouped by namespace URI. The first level is the namespace 105 URI. 106 """ 107 108 allowed_openid_namespaces = [OPENID1_NS, OPENID2_NS] 109
110 - def __init__(self, openid_namespace=None):
111 """Create an empty Message""" 112 self.args = {} 113 self.namespaces = NamespaceMap() 114 if openid_namespace is None: 115 self._openid_ns_uri = None 116 else: 117 self.setOpenIDNamespace(openid_namespace)
118
119 - def fromPostArgs(cls, args):
120 """Construct a Message containing a set of POST arguments""" 121 self = cls() 122 123 # Partition into "openid." args and bare args 124 openid_args = {} 125 for key, value in args.iteritems(): 126 if isinstance(value, list): 127 raise TypeError("query dict must have one value for each key, " 128 "not lists of values. Query is %r" % (args,)) 129 130 131 try: 132 prefix, rest = key.split('.', 1) 133 except ValueError: 134 prefix = None 135 136 if prefix != 'openid': 137 self.args[(BARE_NS, key)] = value 138 else: 139 openid_args[rest] = value 140 141 self._fromOpenIDArgs(openid_args) 142 143 return self
144 145 fromPostArgs = classmethod(fromPostArgs) 146
147 - def fromOpenIDArgs(cls, openid_args):
148 """Construct a Message from a parsed KVForm message""" 149 self = cls() 150 self._fromOpenIDArgs(openid_args) 151 return self
152 153 fromOpenIDArgs = classmethod(fromOpenIDArgs) 154
155 - def _fromOpenIDArgs(self, openid_args):
156 global registered_aliases 157 158 ns_args = [] 159 160 # Resolve namespaces 161 for rest, value in openid_args.iteritems(): 162 try: 163 ns_alias, ns_key = rest.split('.', 1) 164 except ValueError: 165 ns_alias = NULL_NAMESPACE 166 ns_key = rest 167 168 if ns_alias == 'ns': 169 self.namespaces.addAlias(value, ns_key) 170 elif ns_alias == NULL_NAMESPACE and ns_key == 'ns': 171 # null namespace 172 self.namespaces.addAlias(value, NULL_NAMESPACE) 173 else: 174 ns_args.append((ns_alias, ns_key, value)) 175 176 # Ensure that there is an OpenID namespace definition 177 openid_ns_uri = self.namespaces.getNamespaceURI(NULL_NAMESPACE) 178 if openid_ns_uri is None: 179 openid_ns_uri = OPENID1_NS 180 181 self.setOpenIDNamespace(openid_ns_uri) 182 183 # Actually put the pairs into the appropriate namespaces 184 for (ns_alias, ns_key, value) in ns_args: 185 ns_uri = self.namespaces.getNamespaceURI(ns_alias) 186 if ns_uri is None: 187 # Only try to map an alias to a default if it's an 188 # OpenID 1.x message. 189 if openid_ns_uri == OPENID1_NS: 190 for _alias, _uri in registered_aliases.iteritems(): 191 if _alias == ns_alias: 192 ns_uri = _uri 193 break 194 195 if ns_uri is None: 196 ns_uri = openid_ns_uri 197 ns_key = '%s.%s' % (ns_alias, ns_key) 198 else: 199 self.namespaces.addAlias(ns_uri, ns_alias) 200 201 self.setArg(ns_uri, ns_key, value)
202
203 - def setOpenIDNamespace(self, openid_ns_uri):
204 if openid_ns_uri not in self.allowed_openid_namespaces: 205 raise ValueError('Invalid null namespace: %r' % (openid_ns_uri,)) 206 207 self.namespaces.addAlias(openid_ns_uri, NULL_NAMESPACE) 208 self._openid_ns_uri = openid_ns_uri
209
210 - def getOpenIDNamespace(self):
211 return self._openid_ns_uri
212
213 - def isOpenID1(self):
214 return self.getOpenIDNamespace() == OPENID1_NS
215
216 - def isOpenID2(self):
217 return self.getOpenIDNamespace() == OPENID2_NS
218
219 - def fromKVForm(cls, kvform_string):
220 """Create a Message from a KVForm string""" 221 return cls.fromOpenIDArgs(kvform.kvToDict(kvform_string))
222 223 fromKVForm = classmethod(fromKVForm) 224
225 - def copy(self):
226 return copy.deepcopy(self)
227
228 - def toPostArgs(self):
229 """Return all arguments with openid. in front of namespaced arguments. 230 """ 231 args = {} 232 233 # Add namespace definitions to the output 234 for ns_uri, alias in self.namespaces.iteritems(): 235 if alias == NULL_NAMESPACE: 236 if ns_uri != OPENID1_NS: 237 args['openid.ns'] = ns_uri 238 else: 239 # drop the default null namespace definition. This 240 # potentially changes a message since we have no 241 # way of knowing whether it was explicitly 242 # specified at the time the message was 243 # parsed. The vast majority of the time, this will 244 # be the right thing to do. Possibly this could 245 # look in the signed list. 246 pass 247 else: 248 if self.getOpenIDNamespace() != OPENID1_NS: 249 ns_key = 'openid.ns.' + alias 250 args[ns_key] = ns_uri 251 252 for (ns_uri, ns_key), value in self.args.iteritems(): 253 key = self.getKey(ns_uri, ns_key) 254 args[key] = value 255 256 return args
257
258 - def toArgs(self):
259 """Return all namespaced arguments, failing if any 260 non-namespaced arguments exist.""" 261 # FIXME - undocumented exception 262 post_args = self.toPostArgs() 263 kvargs = {} 264 for k, v in post_args.iteritems(): 265 if not k.startswith('openid.'): 266 raise ValueError( 267 'This message can only be encoded as a POST, because it ' 268 'contains arguments that are not prefixed with "openid."') 269 else: 270 kvargs[k[7:]] = v 271 272 return kvargs
273
274 - def toFormMarkup(self, action_url, form_tag_attrs=None, 275 submit_text="Continue"):
276 """Generate HTML form markup that contains the values in this 277 message, to be HTTP POSTed as x-www-form-urlencoded UTF-8. 278 279 @param action_url: The URL to which the form will be POSTed 280 @type action_url: str 281 282 @param form_tag_attrs: Dictionary of attributes to be added to 283 the form tag. 'accept-charset' and 'enctype' have defaults 284 that can be overridden. If a value is supplied for 285 'action' or 'method', it will be replaced. 286 @type form_tag_attrs: {unicode: unicode} 287 288 @param submit_text: The text that will appear on the submit 289 button for this form. 290 @type submit_text: unicode 291 292 @returns: A string containing (X)HTML markup for a form that 293 encodes the values in this Message object. 294 @rtype: str or unicode 295 """ 296 if ElementTree is None: 297 raise RuntimeError('This function requires ElementTree.') 298 299 form = ElementTree.Element('form') 300 301 if form_tag_attrs: 302 for name, attr in form_tag_attrs.iteritems(): 303 form.attrib[name] = attr 304 305 form.attrib['action'] = action_url 306 form.attrib['method'] = 'post' 307 form.attrib['accept-charset'] = 'UTF-8' 308 form.attrib['enctype'] = 'application/x-www-form-urlencoded' 309 310 for name, value in self.toPostArgs().iteritems(): 311 attrs = {'type': 'hidden', 312 'name': name, 313 'value': value} 314 form.append(ElementTree.Element('input', attrs)) 315 316 submit = ElementTree.Element( 317 'input', {'type':'submit', 'value':submit_text}) 318 form.append(submit) 319 320 return ElementTree.tostring(form)
321
322 - def toURL(self, base_url):
323 """Generate a GET URL with the parameters in this message 324 attached as query parameters.""" 325 return oidutil.appendArgs(base_url, self.toPostArgs())
326
327 - def toKVForm(self):
328 """Generate a KVForm string that contains the parameters in 329 this message. This will fail if the message contains arguments 330 outside of the 'openid.' prefix. 331 """ 332 return kvform.dictToKV(self.toArgs())
333
334 - def toURLEncoded(self):
335 """Generate an x-www-urlencoded string""" 336 args = self.toPostArgs().items() 337 args.sort() 338 return urllib.urlencode(args)
339
340 - def _fixNS(self, namespace):
341 """Convert an input value into the internally used values of 342 this object 343 344 @param namespace: The string or constant to convert 345 @type namespace: str or unicode or BARE_NS or OPENID_NS 346 """ 347 if namespace == OPENID_NS: 348 if self._openid_ns_uri is None: 349 raise UndefinedOpenIDNamespace('OpenID namespace not set') 350 else: 351 namespace = self._openid_ns_uri 352 353 if namespace != BARE_NS and type(namespace) not in [str, unicode]: 354 raise TypeError( 355 "Namespace must be BARE_NS, OPENID_NS or a string. got %r" 356 % (namespace,)) 357 358 if namespace != BARE_NS and ':' not in namespace: 359 fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r' 360 warnings.warn(fmt % (namespace,), DeprecationWarning) 361 362 if namespace == 'sreg': 363 fmt = 'Using %r instead of "sreg" as namespace' 364 warnings.warn(fmt % (SREG_URI,), DeprecationWarning,) 365 return SREG_URI 366 367 return namespace
368
369 - def hasKey(self, namespace, ns_key):
370 namespace = self._fixNS(namespace) 371 return (namespace, ns_key) in self.args
372
373 - def getKey(self, namespace, ns_key):
374 """Get the key for a particular namespaced argument""" 375 namespace = self._fixNS(namespace) 376 if namespace == BARE_NS: 377 return ns_key 378 379 ns_alias = self.namespaces.getAlias(namespace) 380 381 # No alias is defined, so no key can exist 382 if ns_alias is None: 383 return None 384 385 if ns_alias == NULL_NAMESPACE: 386 tail = ns_key 387 else: 388 tail = '%s.%s' % (ns_alias, ns_key) 389 390 return 'openid.' + tail
391
392 - def getArg(self, namespace, key, default=None):
393 """Get a value for a namespaced key. 394 395 @param namespace: The namespace in the message for this key 396 @type namespace: str 397 398 @param key: The key to get within this namespace 399 @type key: str 400 401 @param default: The value to use if this key is absent from 402 this message. Using the special value 403 openid.message.no_default will result in this method 404 raising a KeyError instead of returning the default. 405 406 @rtype: str or the type of default 407 @raises KeyError: if default is no_default 408 @raises UndefinedOpenIDNamespace: if the message has not yet 409 had an OpenID namespace set 410 """ 411 namespace = self._fixNS(namespace) 412 args_key = (namespace, key) 413 try: 414 return self.args[args_key] 415 except KeyError: 416 if default is no_default: 417 raise KeyError((namespace, key)) 418 else: 419 return default
420
421 - def getArgs(self, namespace):
422 """Get the arguments that are defined for this namespace URI 423 424 @returns: mapping from namespaced keys to values 425 @returntype: dict 426 """ 427 namespace = self._fixNS(namespace) 428 return dict([ 429 (ns_key, value) 430 for ((pair_ns, ns_key), value) 431 in self.args.iteritems() 432 if pair_ns == namespace 433 ])
434
435 - def updateArgs(self, namespace, updates):
436 """Set multiple key/value pairs in one call 437 438 @param updates: The values to set 439 @type updates: {unicode:unicode} 440 """ 441 namespace = self._fixNS(namespace) 442 for k, v in updates.iteritems(): 443 self.setArg(namespace, k, v)
444
445 - def setArg(self, namespace, key, value):
446 """Set a single argument in this namespace""" 447 namespace = self._fixNS(namespace) 448 self.args[(namespace, key)] = value 449 if not (namespace is BARE_NS): 450 self.namespaces.add(namespace)
451
452 - def delArg(self, namespace, key):
453 namespace = self._fixNS(namespace) 454 del self.args[(namespace, key)]
455
456 - def __str__(self):
457 return "<%s.%s %r>" % (self.__class__.__module__, 458 self.__class__.__name__, 459 self.args)
460
461 - def __eq__(self, other):
462 return self.args == other.args
463 464
465 - def __ne__(self, other):
466 return not (self == other)
467 468
469 - def getAliasedArg(self, aliased_key, default=None):
470 try: 471 alias, key = aliased_key.split('.', 1) 472 except ValueError: 473 # need more than x values to unpack 474 ns = None 475 else: 476 ns = self.namespaces.getNamespaceURI(alias) 477 478 if ns is None: 479 key = aliased_key 480 ns = self.getOpenIDNamespace() 481 482 return self.getArg(ns, key, default)
483
484 -class NamespaceMap(object):
485 """Maintains a bijective map between namespace uris and aliases. 486 """
487 - def __init__(self):
488 self.alias_to_namespace = {} 489 self.namespace_to_alias = {}
490
491 - def getAlias(self, namespace_uri):
492 return self.namespace_to_alias.get(namespace_uri)
493
494 - def getNamespaceURI(self, alias):
495 return self.alias_to_namespace.get(alias)
496
497 - def iterNamespaceURIs(self):
498 """Return an iterator over the namespace URIs""" 499 return iter(self.namespace_to_alias)
500
501 - def iterAliases(self):
502 """Return an iterator over the aliases""" 503 return iter(self.alias_to_namespace)
504
505 - def iteritems(self):
506 """Iterate over the mapping 507 508 @returns: iterator of (namespace_uri, alias) 509 """ 510 return self.namespace_to_alias.iteritems()
511
512 - def addAlias(self, namespace_uri, desired_alias):
513 """Add an alias from this namespace URI to the desired alias 514 """ 515 # Check that desired_alias is not an openid protocol field as 516 # per the spec. 517 assert desired_alias not in OPENID_PROTOCOL_FIELDS, \ 518 "%r is not an allowed namespace alias" % (desired_alias,) 519 520 # Check that desired_alias does not contain a period as per 521 # the spec. 522 if type(desired_alias) in [str, unicode]: 523 assert '.' not in desired_alias, \ 524 "%r must not contain a dot" % (desired_alias,) 525 526 # Check that there is not a namespace already defined for 527 # the desired alias 528 current_namespace_uri = self.alias_to_namespace.get(desired_alias) 529 if (current_namespace_uri is not None 530 and current_namespace_uri != namespace_uri): 531 532 fmt = ('Cannot map %r to alias %r. ' 533 '%r is already mapped to alias %r') 534 535 msg = fmt % ( 536 namespace_uri, 537 desired_alias, 538 current_namespace_uri, 539 desired_alias) 540 raise KeyError(msg) 541 542 # Check that there is not already a (different) alias for 543 # this namespace URI 544 alias = self.namespace_to_alias.get(namespace_uri) 545 if alias is not None and alias != desired_alias: 546 fmt = ('Cannot map %r to alias %r. ' 547 'It is already mapped to alias %r') 548 raise KeyError(fmt % (namespace_uri, desired_alias, alias)) 549 550 assert (desired_alias == NULL_NAMESPACE or 551 type(desired_alias) in [str, unicode]), repr(desired_alias) 552 self.alias_to_namespace[desired_alias] = namespace_uri 553 self.namespace_to_alias[namespace_uri] = desired_alias 554 return desired_alias
555
556 - def add(self, namespace_uri):
557 """Add this namespace URI to the mapping, without caring what 558 alias it ends up with""" 559 # See if this namespace is already mapped to an alias 560 alias = self.namespace_to_alias.get(namespace_uri) 561 if alias is not None: 562 return alias 563 564 # Fall back to generating a numerical alias 565 i = 0 566 while True: 567 alias = 'ext' + str(i) 568 try: 569 self.addAlias(namespace_uri, alias) 570 except KeyError: 571 i += 1 572 else: 573 return alias 574 575 assert False, "Not reached"
576
577 - def isDefined(self, namespace_uri):
578 return namespace_uri in self.namespace_to_alias
579
580 - def __contains__(self, namespace_uri):
581 return self.isDefined(namespace_uri)
582