message.py 21 KB

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