Procházet zdrojové kódy

HUE-1841 [core] Add support for OpenId authentication

William Bourque před 12 roky
rodič
revize
ff158885fc
100 změnil soubory, kde provedl 15289 přidání a 1 odebrání
  1. 2 1
      desktop/Makefile
  2. 20 0
      desktop/conf.dist/hue.ini
  3. 19 0
      desktop/conf/pseudo-distributed.ini.tmpl
  4. 23 0
      desktop/core/ext-py/django-openid-auth-0.5/LICENSE.txt
  5. 8 0
      desktop/core/ext-py/django-openid-auth-0.5/MANIFEST.in
  6. 10 0
      desktop/core/ext-py/django-openid-auth-0.5/Makefile
  7. 31 0
      desktop/core/ext-py/django-openid-auth-0.5/PKG-INFO
  8. 179 0
      desktop/core/ext-py/django-openid-auth-0.5/README.txt
  9. 29 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/__init__.py
  10. 90 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/admin.py
  11. 330 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/auth.py
  12. 68 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/exceptions.py
  13. 87 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/forms.py
  14. 27 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/__init__.py
  15. 27 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/__init__.py
  16. 39 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/openid_cleanup.py
  17. 58 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/models.py
  18. 34 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/signals.py
  19. 131 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/store.py
  20. 411 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/teams.py
  21. 11 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/failure.html
  22. 43 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/login.html
  23. 41 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/__init__.py
  24. 88 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_admin.py
  25. 185 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_auth.py
  26. 193 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_store.py
  27. 1376 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_views.py
  28. 39 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/urls.py
  29. 36 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/urls.py
  30. 313 0
      desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/views.py
  31. 28 0
      desktop/core/ext-py/django-openid-auth-0.5/example_consumer/__init__.py
  32. 11 0
      desktop/core/ext-py/django-openid-auth-0.5/example_consumer/manage.py
  33. 145 0
      desktop/core/ext-py/django-openid-auth-0.5/example_consumer/settings.py
  34. 45 0
      desktop/core/ext-py/django-openid-auth-0.5/example_consumer/urls.py
  35. 57 0
      desktop/core/ext-py/django-openid-auth-0.5/example_consumer/views.py
  36. 80 0
      desktop/core/ext-py/django-openid-auth-0.5/setup.py
  37. 202 0
      desktop/core/ext-py/python-openid-2.2.5/LICENSE
  38. 7 0
      desktop/core/ext-py/python-openid-2.2.5/MANIFEST.in
  39. 225 0
      desktop/core/ext-py/python-openid-2.2.5/NEWS
  40. 4 0
      desktop/core/ext-py/python-openid-2.2.5/NOTICE
  41. 25 0
      desktop/core/ext-py/python-openid-2.2.5/PKG-INFO
  42. 68 0
      desktop/core/ext-py/python-openid-2.2.5/README
  43. 67 0
      desktop/core/ext-py/python-openid-2.2.5/admin/builddiscover.py
  44. 10 0
      desktop/core/ext-py/python-openid-2.2.5/admin/fixperms
  45. 47 0
      desktop/core/ext-py/python-openid-2.2.5/admin/gettlds.py
  46. 2 0
      desktop/core/ext-py/python-openid-2.2.5/admin/makechangelog
  47. 7 0
      desktop/core/ext-py/python-openid-2.2.5/admin/makedoc
  48. 2 0
      desktop/core/ext-py/python-openid-2.2.5/admin/pythonsource
  49. 204 0
      desktop/core/ext-py/python-openid-2.2.5/admin/runtests
  50. 7 0
      desktop/core/ext-py/python-openid-2.2.5/admin/setversion
  51. 13 0
      desktop/core/ext-py/python-openid-2.2.5/admin/tagrelease
  52. 96 0
      desktop/core/ext-py/python-openid-2.2.5/background-associations.txt
  53. 47 0
      desktop/core/ext-py/python-openid-2.2.5/contrib/associate
  54. 151 0
      desktop/core/ext-py/python-openid-2.2.5/contrib/openid-parse
  55. 170 0
      desktop/core/ext-py/python-openid-2.2.5/contrib/upgrade-store-1.1-to-2.0
  56. 91 0
      desktop/core/ext-py/python-openid-2.2.5/examples/README
  57. 507 0
      desktop/core/ext-py/python-openid-2.2.5/examples/consumer.py
  58. 46 0
      desktop/core/ext-py/python-openid-2.2.5/examples/discover
  59. 67 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/README
  60. 3 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/models.py
  61. 9 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/urls.py
  62. 220 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/views.py
  63. 11 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/manage.py
  64. 3 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/models.py
  65. 103 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/tests.py
  66. 12 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/urls.py
  67. 279 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/views.py
  68. 84 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/settings.py
  69. 127 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/index.html
  70. 5 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/request_form.html
  71. 28 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/index.html
  72. 15 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/endpoint.html
  73. 14 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/idPage.html
  74. 51 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/index.html
  75. 21 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/pape_request_info.html
  76. 51 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/trust.html
  77. 17 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/xrds.xml
  78. 8 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/urls.py
  79. 147 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/util.py
  80. 14 0
      desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/views.py
  81. 721 0
      desktop/core/ext-py/python-openid-2.2.5/examples/server.py
  82. 55 0
      desktop/core/ext-py/python-openid-2.2.5/openid/__init__.py
  83. 555 0
      desktop/core/ext-py/python-openid-2.2.5/openid/association.py
  84. 6 0
      desktop/core/ext-py/python-openid-2.2.5/openid/consumer/__init__.py
  85. 1900 0
      desktop/core/ext-py/python-openid-2.2.5/openid/consumer/consumer.py
  86. 470 0
      desktop/core/ext-py/python-openid-2.2.5/openid/consumer/discover.py
  87. 249 0
      desktop/core/ext-py/python-openid-2.2.5/openid/consumer/html_parse.py
  88. 220 0
      desktop/core/ext-py/python-openid-2.2.5/openid/cryptutil.py
  89. 42 0
      desktop/core/ext-py/python-openid-2.2.5/openid/dh.py
  90. 46 0
      desktop/core/ext-py/python-openid-2.2.5/openid/extension.py
  91. 5 0
      desktop/core/ext-py/python-openid-2.2.5/openid/extensions/__init__.py
  92. 774 0
      desktop/core/ext-py/python-openid-2.2.5/openid/extensions/ax.py
  93. 277 0
      desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape2.py
  94. 473 0
      desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape5.py
  95. 518 0
      desktop/core/ext-py/python-openid-2.2.5/openid/extensions/sreg.py
  96. 427 0
      desktop/core/ext-py/python-openid-2.2.5/openid/fetchers.py
  97. 123 0
      desktop/core/ext-py/python-openid-2.2.5/openid/kvform.py
  98. 631 0
      desktop/core/ext-py/python-openid-2.2.5/openid/message.py
  99. 190 0
      desktop/core/ext-py/python-openid-2.2.5/openid/oidutil.py
  100. 6 0
      desktop/core/ext-py/python-openid-2.2.5/openid/server/__init__.py

+ 2 - 1
desktop/Makefile

@@ -43,7 +43,8 @@ APPS := core \
 	libs/hadoop \
 	libs/liboozie \
 	libs/libsaml \
-	libs/librdbms
+	libs/librdbms \
+        libs/libopenid
 
 .PHONY: default
 default:: hue syncdb

+ 20 - 0
desktop/conf.dist/hue.ini

@@ -101,6 +101,7 @@
     # - desktop.auth.backend.RemoteUserDjangoBackend
     # - desktop.auth.backend.OAuthBackend
     # - libsaml.backend.SAML2Backend
+    # - libopenid.backend.OpenIDBackend
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
     ## pam_service=login
@@ -311,6 +312,25 @@
   ## logout_enabled=true
 
 
+###########################################################################
+# Settings to configure OPENID
+###########################################################################
+
+[libopenid]
+  # (Required) OpenId SSO endpoint url. 
+  ## server_endpoint_url=https://www.google.com/accounts/o8/id
+
+  # OpenId 1.1 identity url prefix to be used instead of SSO endpoint url
+  # This is only supported if you are using an OpenId 1.1 endpoint
+  ## identity_url_prefix=https://app.onelogin.com/openid/your_company.com/
+
+  # Create users from OPENID on login.
+  ## create_users_on_login=true
+
+  # Use email for username
+  ## use_email_for_username=true
+
+
 ###########################################################################
 # Settings for the RDBMS application
 ###########################################################################

+ 19 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -317,6 +317,25 @@
   ## logout_enabled=true
 
 
+###########################################################################
+# Settings to configure OPENID
+###########################################################################
+
+[libopenid]
+  # (Required) OpenId SSO endpoint url. 
+  ## server_endpoint_url=https://www.google.com/accounts/o8/id
+
+  # OpenId 1.1 identity url prefix to be used instead of SSO endpoint url
+  # This is only supported if you are using an OpenId 1.1 endpoint
+  ## identity_url_prefix=https://app.onelogin.com/openid/your_company.com/
+
+  # Create users from OPENID on login.
+  ## create_users_on_login=true
+
+  # Use email for username
+  ## use_email_for_username=true
+
+
 ###########################################################################
 # Settings for the RDBMS application
 ###########################################################################

+ 23 - 0
desktop/core/ext-py/django-openid-auth-0.5/LICENSE.txt

@@ -0,0 +1,23 @@
+Copyright (C) 2007 Simon Willison
+Copyright (C) 2008-2010 Canonical Ltd.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 8 - 0
desktop/core/ext-py/django-openid-auth-0.5/MANIFEST.in

@@ -0,0 +1,8 @@
+include Makefile
+include MANIFEST.in
+include LICENSE.txt
+include README.txt
+include TODO.txt
+
+recursive-include django_openid_auth/templates *.html
+recursive-include example_consumer *.py

+ 10 - 0
desktop/core/ext-py/django-openid-auth-0.5/Makefile

@@ -0,0 +1,10 @@
+
+check:
+	PYTHONPATH=$(shell pwd) python example_consumer/manage.py test \
+	   --verbosity=2 django_openid_auth
+
+run-example-consumer:
+	PYTHONPATH=$(shell pwd) python example_consumer/manage.py syncdb
+	PYTHONPATH=$(shell pwd) python example_consumer/manage.py runserver
+
+.PHONY: check run-example-consumer

+ 31 - 0
desktop/core/ext-py/django-openid-auth-0.5/PKG-INFO

@@ -0,0 +1,31 @@
+Metadata-Version: 1.1
+Name: django-openid-auth
+Version: 0.5
+Summary: OpenID integration for django.contrib.auth
+Home-page: https://launchpad.net/django-openid-auth
+Author: Canonical Ltd
+Author-email: UNKNOWN
+License: BSD
+Download-URL: http://launchpad.net/django-openid-auth/trunk/0.5/+download/django-openid-auth-0.5.tar.gz
+Description: A library that can be used to add OpenID support to Django applications.
+        The library integrates with Django's built in authentication system, so
+        most applications require minimal changes to support OpenID llogin. The
+        library also includes the following features:
+          * Basic user details are transferred from the OpenID server via the
+            Simple Registration extension or Attribute Exchange extension.
+          * can be configured to use a fixed OpenID server URL, for use in SSO.
+          * supports the launchpad.net teams extension to get team membership
+            info.
+        
+Platform: any
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires: django (>=1.1.2)
+Requires: openid (>=2.2.0)
+Provides: django_openid_auth

+ 179 - 0
desktop/core/ext-py/django-openid-auth-0.5/README.txt

@@ -0,0 +1,179 @@
+= Django OpenID Authentication Support =
+
+This package provides integration between Django's authentication
+system and OpenID authentication.  It also includes support for using
+a fixed OpenID server endpoint, which can be useful when implementing
+single signon systems.
+
+
+== Basic Installation ==
+
+ 1. Install the Jan Rain Python OpenID library.  It can be found at:
+
+        http://openidenabled.com/python-openid/
+
+    It can also be found in most Linux distributions packaged as
+    "python-openid".  You will need version 2.2.0 or later.
+
+ 2. Add 'django_openid_auth' to INSTALLED_APPS for your application.
+    At a minimum, you'll need the following in there:
+
+        INSTALLED_APPS = (
+            'django.contrib.auth',
+            'django.contrib.contenttypes',
+            'django.contrib.sessions',
+            'django_openid_auth',
+        )
+
+ 3. Add 'django_auth_openid.auth.OpenIDBackend' to
+    AUTHENTICATION_BACKENDS.  This should be in addition to the
+    default ModelBackend:
+
+        AUTHENTICATION_BACKENDS = (
+            'django_openid_auth.auth.OpenIDBackend',
+            'django.contrib.auth.backends.ModelBackend',
+        )
+
+ 4. To create users automatically when a new OpenID is used, add the
+    following to the settings:
+
+        OPENID_CREATE_USERS = True
+
+ 5. To have user details updated from OpenID Simple Registration or
+    Attribute Exchange extension data each time they log in, add the
+    following:
+
+        OPENID_UPDATE_DETAILS_FROM_SREG = True
+
+ 6. Hook up the login URLs to your application's urlconf with
+    something like:
+
+        urlpatterns = patterns('',
+            ...
+            (r'^openid/', include('django_openid_auth.urls')),
+            ...
+        )
+
+ 7. Configure the LOGIN_URL and LOGIN_REDIRECT_URL appropriately for
+    your site:
+
+        LOGIN_URL = '/openid/login/'
+        LOGIN_REDIRECT_URL = '/'
+
+    This will allow pages that use the standard @login_required
+    decorator to use the OpenID login page.
+
+ 8. Rerun "python manage.py syncdb" to add the UserOpenID table to
+    your database.
+
+
+== Configuring Single Sign-On ==
+
+If you only want to accept identities from a single OpenID server and
+that server implemnts OpenID 2.0 identifier select mode, add the
+following setting to your app:
+
+    OPENID_SSO_SERVER_URL = 'server-endpoint-url'
+
+With this setting enabled, the user will not be prompted to enter
+their identity URL, and instead an OpenID authentication request will
+be started with the given server URL.
+
+As an example, to use Launchpad accounts for SSO, you'd use:
+
+     OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
+
+
+== Launchpad Teams Support ==
+
+This library supports the Launchpad Teams OpenID extension.  Using
+this feature, it is possible to map Launchpad team memberships to
+Django group memberships.  It can be configured with:
+
+    OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
+    OPENID_LAUNCHPAD_TEAMS_MAPPING = {
+        'launchpad-team-1': 'django-group-1',
+        'launchpad-team-2': 'django-group-2',
+        }
+
+When a user logs in, they will be added or removed from the relevant
+teams listed in the mapping.
+
+If you have already django-groups and want to map these groups automatically, you can use the OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO variable in your settings.py file.
+
+	OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True
+
+If you use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO, the variable OPENID_LAUNCHPAD_TEAMS_MAPPING will be ignored.
+If you want to exclude some groups from the auto mapping, use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST. This variable has only an effect if OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO is True.
+
+	OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2']
+	
+== External redirect domains ==
+
+By default, redirecting back to an external URL after auth is forbidden. To permit redirection to external URLs on a separate domain, define ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS in your settings.py file as a list of permitted domains:
+
+	ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['example.com', 'example.org']
+
+and redirects to external URLs on those domains will additionally be permitted.
+
+== Use as /admin (django.admin.contrib) login ==
+
+If you require openid authentication into the admin application, add the following setting:
+
+        OPENID_USE_AS_ADMIN_LOGIN = True
+
+It is worth noting that a user needs to be be marked as a "staff user" to be able to access the admin interface.  A new openid user will not normally be a "staff user".  
+The easiest way to resolve this is to use traditional authentication (OPENID_USE_AS_ADMIN_LOGIN = False) to sign in as your first user with a password and authorise your 
+openid user to be staff.
+
+== Change Django usernames if the nickname changes on the provider ==
+
+If you want your Django username to change when a user updates the nickname on their provider, add the following setting:
+
+        OPENID_FOLLOW_RENAMES = True
+
+If the new nickname is available as a Django username, the user is renamed.
+Otherwise the user will be renamed to nickname+i for an incrememnting value of i until no conflict occurs.
+If the user has already been renamed to nickname+1 due to a conflict, and the nickname is still not available, the user will keep their existing username.
+
+== Require a valid nickname ==
+
+If you must have a valid, unique nickname in order to create a user accont, add the following setting:
+
+        OPENID_STRICT_USERNAMES = True
+        
+This will cause an OpenID login attempt to fail if the provider does not return a 'nickname' (username) for the user, or if the nickname conflicts with an existing user with a different openid identiy url.
+Without this setting, logins without a nickname will be given the username 'openiduser', and upon conflicts with existing username, an incrementing number will be appended to the username until it is unique.
+
+== Require Physical Multi-Factor Authentication ==
+
+If your users should use a physical multi-factor authentication method, such as RSA tokens or YubiKey, add the following setting:
+
+        OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True
+        
+If the user's OpenID provider supports the PAPE extension and provides the Physical Multifactor authentication policy, this will
+cause the OpenID login to fail if the user does not provide valid physical authentication to the provider.
+
+== Override Login Failure Handling ==
+
+You can optionally provide your own handler for login failures by adding the following setting:
+
+        OPENID_RENDER_FAILURE = failure_handler_function
+
+Where failure_handler_function is a function reference that will take the following parameters:
+
+        def failure_handler_function(request, message, status=None, template_name=None, exception=None)
+
+This function must return a Django.http.HttpResponse instance.
+
+== Use the user's email for suggested usernames ==
+
+You can optionally strip out non-alphanumeric characters from the user's email
+to generate a preferred username, if the server doesn't provide nick
+information, by setting the following setting:
+
+        OPENID_USE_EMAIL_FOR_USERNAME = True
+
+Otherwise, and by default, if the server omits nick information and a user is
+created it'll receive a username 'openiduser' + a number.
+Consider also the OPENID_STRICT_USERNAMES setting (see ``Require a valid nickname``)

+ 29 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/__init__.py

@@ -0,0 +1,29 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+

+ 90 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/admin.py

@@ -0,0 +1,90 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2008-2013 Canonical Ltd.
+# Copyright (C) 2010 Dave Walker
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.conf import settings
+from django.contrib import admin
+from django_openid_auth.models import Nonce, Association, UserOpenID
+from django_openid_auth.store import DjangoOpenIDStore
+
+
+class NonceAdmin(admin.ModelAdmin):
+    list_display = ('server_url', 'timestamp')
+    actions = ['cleanup_nonces']
+
+    def cleanup_nonces(self, request, queryset):
+        store = DjangoOpenIDStore()
+        count = store.cleanupNonces()
+        self.message_user(request, "%d expired nonces removed" % count)
+    cleanup_nonces.short_description = "Clean up expired nonces"
+
+admin.site.register(Nonce, NonceAdmin)
+
+
+class AssociationAdmin(admin.ModelAdmin):
+    list_display = ('server_url', 'assoc_type')
+    list_filter = ('assoc_type',)
+    search_fields = ('server_url',)
+    actions = ['cleanup_associations']
+
+    def cleanup_associations(self, request, queryset):
+        store = DjangoOpenIDStore()
+        count = store.cleanupAssociations()
+        self.message_user(request, "%d expired associations removed" % count)
+    cleanup_associations.short_description = "Clean up expired associations"
+
+admin.site.register(Association, AssociationAdmin)
+
+
+class UserOpenIDAdmin(admin.ModelAdmin):
+    raw_id_fields = ('user',)
+    list_display = ('user', 'claimed_id')
+    search_fields = ('claimed_id',)
+
+admin.site.register(UserOpenID, UserOpenIDAdmin)
+
+
+# Support for allowing openid authentication for /admin (django.contrib.admin)
+if getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False):
+    from django.http import HttpResponseRedirect
+    from django_openid_auth import views
+
+    def _openid_login(self, request, error_message='', extra_context=None):
+        if request.user.is_authenticated():
+            if not request.user.is_staff:
+                return views.default_render_failure(
+                    request, "User %s does not have admin access."
+                    % request.user.username)
+            assert error_message, "Unknown Error: %s" % error_message
+        else:
+            # Redirect to openid login path,
+            return HttpResponseRedirect(
+                settings.LOGIN_URL + "?next=" + request.get_full_path())
+
+    # Overide the standard admin login form.
+    admin.sites.AdminSite.login = _openid_login

+ 330 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/auth.py

@@ -0,0 +1,330 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""Glue between OpenID and django.contrib.auth."""
+
+__metaclass__ = type
+
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from openid.consumer.consumer import SUCCESS
+from openid.extensions import ax, sreg, pape
+
+from django_openid_auth import teams
+from django_openid_auth.models import UserOpenID
+from django_openid_auth.exceptions import (
+    IdentityAlreadyClaimed,
+    DuplicateUsernameViolation,
+    MissingUsernameViolation,
+    MissingPhysicalMultiFactor,
+    RequiredAttributeNotReturned,
+)
+
+class OpenIDBackend:
+    """A django.contrib.auth backend that authenticates the user based on
+    an OpenID response."""
+
+    supports_object_permissions = False
+    supports_anonymous_user = True
+
+    def get_user(self, user_id):
+        try:
+            return User.objects.get(pk=user_id)
+        except User.DoesNotExist:
+            return None
+
+    def authenticate(self, **kwargs):
+        """Authenticate the user based on an OpenID response."""
+        # Require that the OpenID response be passed in as a keyword
+        # argument, to make sure we don't match the username/password
+        # calling conventions of authenticate.
+
+        openid_response = kwargs.get('openid_response')
+        if openid_response is None:
+            return None
+
+        if openid_response.status != SUCCESS:
+            return None
+
+        user = None
+        try:
+            user_openid = UserOpenID.objects.get(
+                claimed_id__exact=openid_response.identity_url)
+        except UserOpenID.DoesNotExist:
+            if getattr(settings, 'OPENID_CREATE_USERS', False):
+                user = self.create_user_from_openid(openid_response)
+        else:
+            user = user_openid.user
+
+        if user is None:
+            return None
+
+        if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False):
+            details = self._extract_user_details(openid_response)
+            self.update_user_details(user, details, openid_response)
+
+        if getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False):
+            pape_response = pape.Response.fromSuccessResponse(openid_response)
+            if pape_response is None or \
+               pape.AUTH_MULTI_FACTOR_PHYSICAL not in pape_response.auth_policies:
+                raise MissingPhysicalMultiFactor()
+
+        teams_response = teams.TeamsResponse.fromSuccessResponse(
+            openid_response)
+        if teams_response:
+            self.update_groups_from_teams(user, teams_response)
+            self.update_staff_status_from_teams(user, teams_response)
+
+        return user
+
+    def _extract_user_details(self, openid_response):
+        email = fullname = first_name = last_name = nickname = None
+        sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response)
+        if sreg_response:
+            email = sreg_response.get('email')
+            fullname = sreg_response.get('fullname')
+            nickname = sreg_response.get('nickname')
+        # If any attributes are provided via Attribute Exchange, use
+        # them in preference.
+        fetch_response = ax.FetchResponse.fromSuccessResponse(openid_response)
+        if fetch_response:
+            # The myOpenID provider advertises AX support, but uses
+            # attribute names from an obsolete draft of the
+            # specification.  We check for them first so the common
+            # names take precedence.
+            email = fetch_response.getSingle(
+                'http://schema.openid.net/contact/email', email)
+            fullname = fetch_response.getSingle(
+                'http://schema.openid.net/namePerson', fullname)
+            nickname = fetch_response.getSingle(
+                'http://schema.openid.net/namePerson/friendly', nickname)
+
+            email = fetch_response.getSingle(
+                'http://axschema.org/contact/email', email)
+            fullname = fetch_response.getSingle(
+                'http://axschema.org/namePerson', fullname)
+            first_name = fetch_response.getSingle(
+                'http://axschema.org/namePerson/first', first_name)
+            last_name = fetch_response.getSingle(
+                'http://axschema.org/namePerson/last', last_name)
+            nickname = fetch_response.getSingle(
+                'http://axschema.org/namePerson/friendly', nickname)
+
+        if fullname and not (first_name or last_name):
+            # Django wants to store first and last names separately,
+            # so we do our best to split the full name.
+            fullname = fullname.strip()
+            split_names = fullname.rsplit(None, 1)
+            if len(split_names) == 2:
+                first_name, last_name = split_names
+            else:
+                first_name = u''
+                last_name = fullname
+
+        return dict(email=email, nickname=nickname,
+                    first_name=first_name, last_name=last_name)
+
+    def _get_preferred_username(self, nickname, email):
+        if nickname:
+            return nickname
+        if email and getattr(settings, 'OPENID_USE_EMAIL_FOR_USERNAME',
+            False):
+            suggestion = ''.join([x for x in email if x.isalnum()])
+            if suggestion:
+                return suggestion
+        return 'openiduser'
+
+    def _get_available_username(self, nickname, identity_url):
+        # If we're being strict about usernames, throw an error if we didn't
+        # get one back from the provider
+        if getattr(settings, 'OPENID_STRICT_USERNAMES', False):
+            if nickname is None or nickname == '':
+                raise MissingUsernameViolation()
+                
+        # If we don't have a nickname, and we're not being strict, use a default
+        nickname = nickname or 'openiduser'
+
+        # See if we already have this nickname assigned to a username
+        try:
+            user = User.objects.get(username__exact=nickname)
+        except User.DoesNotExist:
+            # No conflict, we can use this nickname
+            return nickname
+
+        # Check if we already have nickname+i for this identity_url
+        try:
+            user_openid = UserOpenID.objects.get(
+                claimed_id__exact=identity_url,
+                user__username__startswith=nickname)
+            # No exception means we have an existing user for this identity
+            # that starts with this nickname.
+            
+            # If they are an exact match, the user already exists and hasn't
+            # changed their username, so continue to use it
+            if nickname == user_openid.user.username:
+                return nickname
+            
+            # It is possible we've had to assign them to nickname+i already.
+            oid_username = user_openid.user.username
+            if len(oid_username) > len(nickname):
+                try:
+                    # check that it ends with a number
+                    int(oid_username[len(nickname):])
+                    return oid_username
+                except ValueError:
+                    # username starts with nickname, but isn't nickname+#
+                    pass
+        except UserOpenID.DoesNotExist:
+            # No user associated with this identity_url
+            pass
+
+
+        if getattr(settings, 'OPENID_STRICT_USERNAMES', False):
+            if User.objects.filter(username__exact=nickname).count() > 0:
+                raise DuplicateUsernameViolation(
+                    "The username (%s) with which you tried to log in is "
+                    "already in use for a different account." % nickname)
+
+        # Pick a username for the user based on their nickname,
+        # checking for conflicts.  Start with number of existing users who's
+        # username starts with this nickname to avoid having to iterate over
+        # all of the existing ones.
+        i = User.objects.filter(username__startswith=nickname).count() + 1
+        while True:
+            username = nickname
+            if i > 1:
+                username += str(i)
+            try:
+                user = User.objects.get(username__exact=username)
+            except User.DoesNotExist:
+                break
+            i += 1
+        return username
+
+    def create_user_from_openid(self, openid_response):
+        details = self._extract_user_details(openid_response)
+        required_attrs = getattr(settings, 'OPENID_SREG_REQUIRED_FIELDS', [])
+        if getattr(settings, 'OPENID_STRICT_USERNAMES', False):
+            required_attrs.append('nickname')
+
+        for required_attr in required_attrs:
+            if required_attr not in details or not details[required_attr]:
+                raise RequiredAttributeNotReturned(
+                    "An attribute required for logging in was not "
+                    "returned ({0}).".format(required_attr))
+
+        nickname = self._get_preferred_username(details['nickname'],
+            details['email'])
+        email = details['email'] or ''
+
+        username = self._get_available_username(nickname,
+            openid_response.identity_url)
+
+        user = User.objects.create_user(username, email, password=None)
+        self.associate_openid(user, openid_response)
+        self.update_user_details(user, details, openid_response)
+
+        return user
+
+    def associate_openid(self, user, openid_response):
+        """Associate an OpenID with a user account."""
+        # Check to see if this OpenID has already been claimed.
+        try:
+            user_openid = UserOpenID.objects.get(
+                claimed_id__exact=openid_response.identity_url)
+        except UserOpenID.DoesNotExist:
+            user_openid = UserOpenID(
+                user=user,
+                claimed_id=openid_response.identity_url,
+                display_id=openid_response.endpoint.getDisplayIdentifier())
+            user_openid.save()
+        else:
+            if user_openid.user != user:
+                raise IdentityAlreadyClaimed(
+                    "The identity %s has already been claimed"
+                    % openid_response.identity_url)
+
+        return user_openid
+
+    def update_user_details(self, user, details, openid_response):
+        updated = False
+        if details['first_name']:
+            user.first_name = details['first_name'][:30]
+            updated = True
+        if details['last_name']:
+            user.last_name = details['last_name'][:30]
+            updated = True
+        if details['email']:
+            user.email = details['email']
+            updated = True
+        if getattr(settings, 'OPENID_FOLLOW_RENAMES', False):
+            user.username = self._get_available_username(details['nickname'], openid_response.identity_url)
+            updated = True
+
+        if updated:
+            user.save()
+
+    def update_groups_from_teams(self, user, teams_response):
+        teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False)
+        teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', [])
+        teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+        if teams_mapping_auto:
+            #ignore teams_mapping. use all django-groups
+            teams_mapping = dict()
+            all_groups = Group.objects.exclude(name__in=teams_mapping_auto_blacklist)
+            for group in all_groups:
+                teams_mapping[group.name] = group.name
+
+        if len(teams_mapping) == 0:
+            return
+
+        current_groups = set(user.groups.filter(
+                name__in=teams_mapping.values()))
+        desired_groups = set(Group.objects.filter(
+                name__in=[teams_mapping[lp_team]
+                          for lp_team in teams_response.is_member
+                          if lp_team in teams_mapping]))
+        for group in current_groups - desired_groups:
+            user.groups.remove(group)
+        for group in desired_groups - current_groups:
+            user.groups.add(group)
+
+    def update_staff_status_from_teams(self, user, teams_response):
+        if not hasattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS'):
+            return
+
+        staff_teams = getattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS', [])
+        user.is_staff = False
+
+        for lp_team in teams_response.is_member:
+            if lp_team in staff_teams:
+                user.is_staff = True
+                break
+
+        user.save()
+

+ 68 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/exceptions.py

@@ -0,0 +1,68 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""Exception classes thrown by OpenID Authentication and Validation."""
+
+class DjangoOpenIDException(Exception):
+    pass
+
+class RequiredAttributeNotReturned(DjangoOpenIDException):
+    pass
+
+class IdentityAlreadyClaimed(DjangoOpenIDException):
+
+    def __init__(self, message=None):
+        if message is None:
+            self.message = "Another user already exists for your selected OpenID"
+        else:
+            self.message = message
+
+class DuplicateUsernameViolation(DjangoOpenIDException):
+
+    def __init__(self, message=None):
+        if message is None:
+            self.message = "Your desired username is already being used."
+        else:
+            self.message = message
+
+class MissingUsernameViolation(DjangoOpenIDException):
+
+    def __init__(self, message=None):
+        if message is None:
+            self.message = "No nickname given for your account."
+        else:
+            self.message = message
+
+class MissingPhysicalMultiFactor(DjangoOpenIDException):
+
+    def __init__(self, message=None):
+        if message is None:
+            self.message = "Login requires physical multi-factor authentication."
+        else:
+            self.message = message
+

+ 87 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/forms.py

@@ -0,0 +1,87 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django import forms
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.forms import UserChangeForm
+from django.contrib.auth.models import Group
+from django.utils.translation import ugettext as _
+from django.conf import settings
+
+from openid.yadis import xri
+
+
+def teams_new_unicode(self):
+    """
+    Replacement for Group.__unicode__()
+    Calls original method to chain results
+    """
+    name = self.unicode_before_teams()
+    teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+    group_teams = [t for t in teams_mapping if teams_mapping[t] == self.name]
+    if len(group_teams) > 0:
+        return "%s -> %s" % (name, ", ".join(group_teams))
+    else:
+        return name
+Group.unicode_before_teams = Group.__unicode__
+Group.__unicode__ = teams_new_unicode
+
+
+class UserChangeFormWithTeamRestriction(UserChangeForm):
+    """
+    Extends UserChangeForm to add teams awareness to the user admin form
+    """
+    def clean_groups(self):
+        data = self.cleaned_data['groups']
+        teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+        known_teams = teams_mapping.values()
+        user_groups = self.instance.groups.all()
+        for group in data:
+            if group.name in known_teams and group not in user_groups:
+                raise forms.ValidationError("""The group %s is mapped to an
+                    external team.  You cannot assign it manually.""" % group.name)
+        return data
+UserAdmin.form = UserChangeFormWithTeamRestriction
+
+
+class OpenIDLoginForm(forms.Form):
+    openid_identifier = forms.CharField(
+        max_length=255,
+        widget=forms.TextInput(attrs={'class': 'required openid'}))
+
+    def clean_openid_identifier(self):
+        if 'openid_identifier' in self.cleaned_data:
+            openid_identifier = self.cleaned_data['openid_identifier']
+            if xri.identifierScheme(openid_identifier) == 'XRI' and getattr(
+                settings, 'OPENID_DISALLOW_INAMES', False
+                ):
+                raise forms.ValidationError(_('i-names are not supported'))
+            return self.cleaned_data['openid_identifier']
+
+

+ 27 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/__init__.py

@@ -0,0 +1,27 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.

+ 27 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/__init__.py

@@ -0,0 +1,27 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.

+ 39 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/openid_cleanup.py

@@ -0,0 +1,39 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.core.management.base import NoArgsCommand
+
+from django_openid_auth.store import DjangoOpenIDStore
+
+
+class Command(NoArgsCommand):
+    help = 'Clean up stale OpenID associations and nonces'
+
+    def handle_noargs(self, **options):
+        store = DjangoOpenIDStore()
+        store.cleanup()

+ 58 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/models.py

@@ -0,0 +1,58 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.contrib.auth.models import User
+from django.db import models
+
+
+class Nonce(models.Model):
+    server_url = models.CharField(max_length=2047)
+    timestamp = models.IntegerField()
+    salt = models.CharField(max_length=40)
+
+    def __unicode__(self):
+        return u"Nonce: %s, %s" % (self.server_url, self.salt)
+
+
+class Association(models.Model):
+    server_url = models.TextField(max_length=2047)
+    handle = models.CharField(max_length=255)
+    secret = models.TextField(max_length=255) # Stored base64 encoded
+    issued = models.IntegerField()
+    lifetime = models.IntegerField()
+    assoc_type = models.TextField(max_length=64)
+
+    def __unicode__(self):
+        return u"Association: %s, %s" % (self.server_url, self.handle)
+
+
+class UserOpenID(models.Model):
+    user = models.ForeignKey(User)
+    claimed_id = models.TextField(max_length=2047, unique=True)
+    display_id = models.TextField(max_length=2047)

+ 34 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/signals.py

@@ -0,0 +1,34 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import django.dispatch
+
+
+openid_login_complete = django.dispatch.Signal(providing_args=[
+    'request', 'openid_response'])

+ 131 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/store.py

@@ -0,0 +1,131 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import base64
+import time
+
+from openid.association import Association as OIDAssociation
+from openid.store.interface import OpenIDStore
+from openid.store.nonce import SKEW
+
+from django_openid_auth.models import Association, Nonce
+
+
+class DjangoOpenIDStore(OpenIDStore):
+    def __init__(self):
+        self.max_nonce_age = 6 * 60 * 60 # Six hours
+
+    def storeAssociation(self, server_url, association):
+        try:
+            assoc = Association.objects.get(
+                server_url=server_url, handle=association.handle)
+        except Association.DoesNotExist:
+            assoc = Association(
+                server_url=server_url,
+                handle=association.handle,
+                secret=base64.encodestring(association.secret),
+                issued=association.issued,
+                lifetime=association.lifetime,
+                assoc_type=association.assoc_type)
+        else:
+            assoc.secret = base64.encodestring(association.secret)
+            assoc.issued = association.issued
+            assoc.lifetime = association.lifetime
+            assoc.assoc_type = association.assoc_type
+        assoc.save()
+
+    def getAssociation(self, server_url, handle=None):
+        assocs = []
+        if handle is not None:
+            assocs = Association.objects.filter(
+                server_url=server_url, handle=handle)
+        else:
+            assocs = Association.objects.filter(server_url=server_url)
+        associations = []
+        expired = []
+        for assoc in assocs:
+            association = OIDAssociation(
+                assoc.handle, base64.decodestring(assoc.secret), assoc.issued,
+                assoc.lifetime, assoc.assoc_type
+            )
+            if association.getExpiresIn() == 0:
+                expired.append(assoc)
+            else:
+                associations.append((association.issued, association))
+        for assoc in expired:
+            assoc.delete()
+        if not associations:
+            return None
+        associations.sort()
+        return associations[-1][1]
+
+    def removeAssociation(self, server_url, handle):
+        assocs = list(Association.objects.filter(
+            server_url=server_url, handle=handle))
+        assocs_exist = len(assocs) > 0
+        for assoc in assocs:
+            assoc.delete()
+        return assocs_exist
+
+    def useNonce(self, server_url, timestamp, salt):
+        if abs(timestamp - time.time()) > SKEW:
+            return False
+
+        try:
+            ononce = Nonce.objects.get(
+                server_url__exact=server_url,
+                timestamp__exact=timestamp,
+                salt__exact=salt)
+        except Nonce.DoesNotExist:
+            ononce = Nonce(
+                server_url=server_url,
+                timestamp=timestamp,
+                salt=salt)
+            ononce.save()
+            return True
+
+        return False
+
+    def cleanupNonces(self, _now=None):
+        if _now is None:
+            _now = int(time.time())
+        expired = Nonce.objects.filter(timestamp__lt=_now - SKEW)
+        count = expired.count()
+        if count:
+            expired.delete()
+        return count
+
+    def cleanupAssociations(self):
+        now = int(time.time())
+        expired = Association.objects.extra(
+            where=['issued + lifetime < %d' % now])
+        count = expired.count()
+        if count:
+            expired.delete()
+        return count

+ 411 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/teams.py

@@ -0,0 +1,411 @@
+# Launchpad OpenID Teams Extension support for python-openid
+#
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""Team membership support for Launchpad.
+
+The primary form of communication between the RP and Launchpad is an
+OpenID authentication request. Our solution is to piggyback a team
+membership test onto this interaction.
+
+As part of an OpenID authentication request, the RP includes the
+following fields:
+
+  openid.ns.lp:
+    An OpenID 2.0 namespace URI for the extension. It is not strictly
+    required for 1.1 requests, but including it is good for forward
+    compatibility.
+
+    It must be set to: http://ns.launchpad.net/2007/openid-teams
+
+  openid.lp.query_membership:
+    A comma separated list of Launchpad team names that the RP is
+    interested in.
+
+As part of the positive assertion OpenID response, the following field
+will be provided:
+
+  openid.ns.lp:
+    (as above)
+
+  openid.lp.is_member:
+    A comma separated list of teams that the user is actually a member
+    of. The list may be limited to those teams mentioned in the
+    request.
+
+    This field must be included in the response signature in order to
+    be considered valid (as the response is bounced through the user's
+    web browser, an unsigned value could be modified).
+
+@since: 2.1.1
+"""
+
+from openid.message import registerNamespaceAlias, \
+     NamespaceAliasRegistrationError
+from openid.extension import Extension
+from openid import oidutil
+
+try:
+    basestring #pylint:disable-msg=W0104
+except NameError:
+    # For Python 2.2
+    basestring = (str, unicode) #pylint:disable-msg=W0622
+
+__all__ = [
+    'TeamsRequest',
+    'TeamsResponse',
+    'ns_uri',
+    'supportsTeams',
+    ]
+
+ns_uri = 'http://ns.launchpad.net/2007/openid-teams'
+
+try:
+    registerNamespaceAlias(ns_uri, 'lp')
+except NamespaceAliasRegistrationError, e:
+    oidutil.log('registerNamespaceAlias(%r, %r) failed: %s' % (ns_uri,
+                                                               'lp', str(e),))
+
+def supportsTeams(endpoint):
+    """Does the given endpoint advertise support for Launchpad Teams?
+
+    @param endpoint: The endpoint object as returned by OpenID discovery
+    @type endpoint: openid.consumer.discover.OpenIDEndpoint
+
+    @returns: Whether an lp type was advertised by the endpoint
+    @rtype: bool
+    """
+    return endpoint.usesExtension(ns_uri)
+
+class TeamsNamespaceError(ValueError):
+    """The Launchpad teams namespace was not found and could not
+    be created using the expected name (there's another extension
+    using the name 'lp')
+
+    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 getTeamsNS(message):
+    """Extract the Launchpad teams namespace URI from the given
+    OpenID message.
+
+    @param message: The OpenID message from which to parse Launchpad
+        teams. This may be a request or response message.
+    @type message: C{L{openid.message.Message}}
+
+    @returns: the lp namespace URI for the supplied message. The
+        message may be modified to define a Launchpad teams
+        namespace.
+    @rtype: C{str}
+
+    @raise ValueError: when using OpenID 1 if the message defines
+        the 'lp' alias to be something other than a Launchpad
+        teams type.
+    """
+    # See if there exists an alias for the Launchpad teams type.
+    alias = message.namespaces.getAlias(ns_uri)
+    if alias is None:
+        # There is no alias, so try to add one. (OpenID version 1)
+        try:
+            message.namespaces.addAlias(ns_uri, 'lp')
+        except KeyError, why:
+            # An alias for the string 'lp' already exists, but it's
+            # defined for something other than Launchpad teams
+            raise TeamsNamespaceError(why[0])
+
+    # we know that ns_uri defined, because it's defined in the
+    # else clause of the loop as well, so disable the warning
+    return ns_uri #pylint:disable-msg=W0631
+
+class TeamsRequest(Extension):
+    """An object to hold the state of a Launchpad teams request.
+
+    @ivar query_membership: A comma separated list of Launchpad team
+        names that the RP is interested in.
+    @type required: [str]
+
+    @group Consumer: requestField, requestTeams, getExtensionArgs, addToOpenIDRequest
+    @group Server: fromOpenIDRequest, parseExtensionArgs
+    """
+
+    ns_alias = 'lp'
+
+    def __init__(self, query_membership=None, lp_ns_uri=ns_uri):
+        """Initialize an empty Launchpad teams request"""
+        Extension.__init__(self)
+        self.query_membership = []
+        self.ns_uri = lp_ns_uri
+
+        if query_membership:
+            self.requestTeams(query_membership)
+
+    # Assign getTeamsNS to a static method so that it can be
+    # overridden for testing.
+    _getTeamsNS = staticmethod(getTeamsNS)
+
+    def fromOpenIDRequest(cls, request):
+        """Create a Launchpad teams 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 Launchpad teams request
+        @rtype: C{L{TeamsRequest}}
+        """
+        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._getTeamsNS(message)
+        args = message.getArgs(self.ns_uri)
+        self.parseExtensionArgs(args)
+
+        return self
+
+    fromOpenIDRequest = classmethod(fromOpenIDRequest)
+
+    def parseExtensionArgs(self, args, strict=False):
+        """Parse the unqualified Launchpad teams request
+        parameters and add them to this object.
+
+        This method is essentially the inverse of
+        C{L{getExtensionArgs}}. This method restores the serialized
+        Launchpad teams 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 lp 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 Launchpad teams arguments
+        @type args: {str:str}
+
+        @param strict: Whether requests with fields that are not
+            defined in the Launchpad teams specification should be
+            tolerated (and ignored)
+        @type strict: bool
+
+        @returns: None; updates this object
+        """
+        items = args.get('query_membership')
+        if items:
+            for team_name in items.split(','):
+                try:
+                    self.requestTeam(team_name, strict)
+                except ValueError:
+                    if strict:
+                        raise
+
+    def allRequestedTeams(self):
+        """A list of all of the Launchpad teams that were
+        requested.
+
+        @rtype: [str]
+        """
+        return self.query_membership
+
+    def wereTeamsRequested(self):
+        """Have any Launchpad teams been requested?
+
+        @rtype: bool
+        """
+        return bool(self.allRequestedTeams())
+
+    def __contains__(self, team_name):
+        """Was this team in the request?"""
+        return team_name in self.query_membership
+
+    def requestTeam(self, team_name, strict=False):
+        """Request the specified team from the OpenID user
+
+        @param team_name: the unqualified Launchpad team name
+        @type team_name: str
+
+        @param strict: whether to raise an exception when a team is
+            added to a request more than once
+
+        @raise ValueError: when strict is set and the team was
+            requested more than once
+        """
+        if strict:
+            if team_name in self.query_membership:
+                raise ValueError('That team has already been requested')
+        else:
+            if team_name in self.query_membership:
+                return
+
+        self.query_membership.append(team_name)
+
+    def requestTeams(self, query_membership, strict=False):
+        """Add the given list of teams to the request
+
+        @param query_membership: The Launchpad teams request
+        @type query_membership: [str]
+
+        @raise ValueError: when a team requested is not a string
+            or strict is set and a team was requested more than once
+        """
+        if isinstance(query_membership, basestring):
+            raise TypeError('Teams should be passed as a list of '
+                            'strings (not %r)' % (type(query_membership),))
+
+        for team_name in query_membership:
+            self.requestTeam(team_name, strict=strict)
+
+    def getExtensionArgs(self):
+        """Get a dictionary of unqualified Launchpad teams
+        arguments representing this request.
+
+        This method is essentially the inverse of
+        C{L{parseExtensionArgs}}. This method serializes the Launchpad
+        teams request fields.
+
+        @rtype: {str:str}
+        """
+        args = {}
+
+        if self.query_membership:
+            args['query_membership'] = ','.join(self.query_membership)
+
+        return args
+
+class TeamsResponse(Extension):
+    """Represents the data returned in a Launchpad teams 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 Launchpad teams data, an array.
+
+    @ivar ns_uri: The URI under which the Launchpad teams 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 = 'lp'
+
+    def __init__(self, is_member=None, lp_ns_uri=ns_uri):
+        Extension.__init__(self)
+        if is_member is None:
+            self.is_member = []
+        else:
+            self.is_member = is_member
+
+        self.ns_uri = lp_ns_uri
+
+    def addTeam(self, team_name):
+        if team_name not in self.is_member:
+            self.is_member.append(team_name)
+
+    def extractResponse(cls, request, is_member_str):
+        """Take a C{L{TeamsRequest}} and a list of Launchpad
+        team values and create a C{L{TeamsResponse}}
+        object containing that data.
+
+        @param request: The Launchpad teams request object
+        @type request: TeamsRequest
+
+        @param is_member: The Launchpad teams data for this
+            response, as a list of strings.
+        @type is_member: {str:str}
+
+        @returns: a Launchpad teams response object
+        @rtype: TeamsResponse
+        """
+        self = cls()
+        self.ns_uri = request.ns_uri
+        self.is_member = is_member_str.split(',')
+        return self
+
+    extractResponse = classmethod(extractResponse)
+
+    # Assign getTeamsNS to a static method so that it can be
+    # overridden for testing
+    _getTeamsNS = staticmethod(getTeamsNS)
+
+    def fromSuccessResponse(cls, success_response, signed_only=True):
+        """Create a C{L{TeamsResponse}} 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: TeamsResponse
+        @returns: A Launchpad teams response containing the data
+            that was supplied with the C{id_res} response.
+        """
+        self = cls()
+        self.ns_uri = self._getTeamsNS(success_response.message)
+        if signed_only:
+            args = success_response.getSignedNS(self.ns_uri)
+        else:
+            args = success_response.message.getArgs(self.ns_uri)
+
+        if "is_member" in args:
+            is_member_str = args["is_member"]
+            self.is_member = is_member_str.split(',')
+            #self.is_member = args["is_member"]
+
+        return self
+
+    fromSuccessResponse = classmethod(fromSuccessResponse)
+
+    def getExtensionArgs(self):
+        """Get the fields to put in the Launchpad teams namespace
+        when adding them to an id_res message.
+
+        @see: openid.extension
+        """
+        ns_args = {'is_member': ','.join(self.is_member),}
+        return ns_args
+

+ 11 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/failure.html

@@ -0,0 +1,11 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+    <title>OpenID failed</title>
+</head>
+<body>
+    <h1>OpenID failed</h1>
+    <p>{{ message|escape }}</p>
+</body>
+</html>

+ 43 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/login.html

@@ -0,0 +1,43 @@
+{% load i18n %}
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Sign in with your OpenID</title>
+<style type="text/css">
+input.openid {
+    background: url({% url openid-logo %}) no-repeat;
+    background-position: 0 50%;
+    padding-left: 16px;
+}
+</style>
+</head>
+<body>
+<h1>Sign in with your OpenID</h1>
+{% if form.errors %}
+<p class="errors">{% trans "Please correct errors below:" %}<br />
+    {% if form.openid_identifier.errors %}
+    <span class="error">{{ form.openid_identifier.errors|join:", " }}</span>
+    {% endif %}
+    {% if form.next.errors %}
+    <span class="error">{{ form.next.errors|join:", " }}</span>
+    {% endif %}
+</p>
+{% endif %}
+<form name="fopenid" action="{{ action }}" method="post">
+    {% csrf_token %}
+    <fieldset>
+        <legend>{% trans "Sign In Using Your OpenID" %}</legend>
+        <div class="form-row">
+            <label for="id_openid_identifier">{% trans "OpenID:" %}</label><br />
+            {{ form.openid_identifier }}
+        </div>
+        <div class="submit-row ">
+            <input name="bsignin" type="submit" value="{% trans "Sign in" %}">
+        </div>
+ {% if next %}
+        <input type="hidden" name="next" value="{{ next }}" />
+ {% endif %}
+    </fieldset>
+</form>
+</body>
+</html>

+ 41 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/__init__.py

@@ -0,0 +1,41 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+from test_views import *
+from test_store import *
+from test_auth import *
+from test_admin import *
+
+
+def suite():
+    suite = unittest.TestSuite()
+    for name in ['test_auth', 'test_store', 'test_views', 'test_admin']:
+        mod = __import__('%s.%s' % (__name__, name), {}, {}, ['suite'])
+        suite.addTest(mod.suite())
+    return suite

+ 88 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_admin.py

@@ -0,0 +1,88 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+"""
+Tests for the django_openid_auth Admin login form replacement.
+"""
+
+import os
+import unittest
+
+from django.conf import settings
+from django.contrib.auth.models import User, AnonymousUser
+
+settings.OPENID_USE_AS_ADMIN_LOGIN = True
+from django_openid_auth import admin
+
+from django.test import TestCase
+
+
+def create_user(is_staff=False, authenticated=True):
+    """
+    Create and return a user, either the AnonymousUser or a normal Django user,
+    setting the is_staff attribute if appropriate.
+    """
+    if not authenticated:
+        return AnonymousUser()
+    else:
+        user = User(
+            username=u'testing', email='testing@example.com',
+            is_staff=is_staff)
+        user.set_password(u'test')
+        user.save()
+
+
+class SiteAdminTests(TestCase):
+    """
+    TestCase for accessing /admin/ when the django_openid_auth form replacement
+    is in use.
+    """
+
+    def test_admin_site_with_openid_login_authenticated_non_staff(self):
+        """
+        If the request has an authenticated user, who is not flagged as a
+        staff member, then they get a failure response.
+        """
+        create_user()
+        self.client.login(username='testing', password='test')
+        response = self.client.get('/admin/')
+        self.assertTrue('User testing does not have admin access.' in
+                        response.content, 'Missing error message in response')
+
+    def test_admin_site_with_openid_login_non_authenticated_user(self):
+        """
+        Unauthenticated users accessing the admin page should be directed to
+        the OpenID login url.
+        """
+        response = self.client.get('/admin/')
+        self.assertEqual(302, response.status_code)
+        self.assertEqual('http://testserver/openid/login/?next=/admin/',
+                         response['Location'])
+
+
+def suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)

+ 185 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_auth.py

@@ -0,0 +1,185 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2010-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from django_openid_auth.auth import OpenIDBackend
+from openid.consumer.consumer import SuccessResponse
+from openid.consumer.discover import OpenIDServiceEndpoint
+from openid.message import Message, OPENID2_NS
+
+
+SREG_NS = "http://openid.net/sreg/1.0"
+AX_NS = "http://openid.net/srv/ax/1.0"
+
+class OpenIDBackendTests(TestCase):
+
+    def setUp(self):
+        super(OpenIDBackendTests, self).setUp()
+        self.backend = OpenIDBackend()
+        self.old_openid_use_email_for_username = getattr(settings,
+            'OPENID_USE_EMAIL_FOR_USERNAME', False)
+
+    def tearDown(self):
+        settings.OPENID_USE_EMAIL_FOR_USERNAME = \
+            self.old_openid_use_email_for_username
+
+    def test_extract_user_details_sreg(self):
+        endpoint = OpenIDServiceEndpoint()
+        message = Message(OPENID2_NS)
+        message.setArg(SREG_NS, "nickname", "someuser")
+        message.setArg(SREG_NS, "fullname", "Some User")
+        message.setArg(SREG_NS, "email", "foo@example.com")
+        response = SuccessResponse(
+            endpoint, message, signed_fields=message.toPostArgs().keys())
+
+        data = self.backend._extract_user_details(response)
+        self.assertEqual(data, {"nickname": "someuser",
+                                "first_name": "Some",
+                                "last_name": "User",
+                                "email": "foo@example.com"})
+
+    def make_response_ax(self, schema="http://axschema.org/",
+        fullname="Some User", nickname="someuser", email="foo@example.com",
+        first=None, last=None):
+        endpoint = OpenIDServiceEndpoint()
+        message = Message(OPENID2_NS)
+        attributes = [
+            ("nickname", schema + "namePerson/friendly", nickname),
+            ("fullname", schema + "namePerson", fullname),
+            ("email", schema + "contact/email", email),
+            ]
+        if first:
+            attributes.append(
+                ("first", "http://axschema.org/namePerson/first", first))
+        if last:
+            attributes.append(
+                ("last", "http://axschema.org/namePerson/last", last))
+
+        message.setArg(AX_NS, "mode", "fetch_response")
+        for (alias, uri, value) in attributes:
+            message.setArg(AX_NS, "type.%s" % alias, uri)
+            message.setArg(AX_NS, "value.%s" % alias, value)
+        return SuccessResponse(
+            endpoint, message, signed_fields=message.toPostArgs().keys())
+
+    def test_extract_user_details_ax(self):
+        response = self.make_response_ax(fullname="Some User",
+            nickname="someuser", email="foo@example.com")
+
+        data = self.backend._extract_user_details(response)
+
+        self.assertEqual(data, {"nickname": "someuser",
+                                "first_name": "Some",
+                                "last_name": "User",
+                                "email": "foo@example.com"})
+
+    def test_extract_user_details_ax_split_name(self):
+        # Include fullname too to show that the split data takes
+        # precedence.
+        response = self.make_response_ax(
+            fullname="Bad Data", first="Some", last="User")
+
+        data = self.backend._extract_user_details(response)
+
+        self.assertEqual(data, {"nickname": "someuser",
+                                "first_name": "Some",
+                                "last_name": "User",
+                                "email": "foo@example.com"})
+
+    def test_extract_user_details_ax_broken_myopenid(self):
+        response = self.make_response_ax(
+            schema="http://schema.openid.net/", fullname="Some User",
+            nickname="someuser", email="foo@example.com")
+
+        data = self.backend._extract_user_details(response)
+
+        self.assertEqual(data, {"nickname": "someuser",
+                                "first_name": "Some",
+                                "last_name": "User",
+                                "email": "foo@example.com"})
+
+    def test_update_user_details_long_names(self):
+        response = self.make_response_ax()
+        user = User.objects.create_user('someuser', 'someuser@example.com',
+            password=None)
+        data = dict(first_name=u"Some56789012345678901234567890123",
+            last_name=u"User56789012345678901234567890123",
+            email=u"someotheruser@example.com")
+
+        self.backend.update_user_details(user, data, response)
+
+        self.assertEqual("Some56789012345678901234567890",  user.first_name)
+        self.assertEqual("User56789012345678901234567890",  user.last_name)
+
+    def test_extract_user_details_name_with_trailing_space(self):
+        response = self.make_response_ax(fullname="SomeUser ")
+
+        data = self.backend._extract_user_details(response)
+
+        self.assertEqual("", data['first_name'])
+        self.assertEqual("SomeUser", data['last_name'])
+
+    def test_extract_user_details_name_with_thin_space(self):
+        response = self.make_response_ax(fullname=u"Some\u2009User")
+
+        data = self.backend._extract_user_details(response)
+
+        self.assertEqual("Some", data['first_name'])
+        self.assertEqual("User", data['last_name'])
+
+    def test_preferred_username_email_munging(self):
+        settings.OPENID_USE_EMAIL_FOR_USERNAME = True
+        for nick, email, expected in [
+            ('nickcomesfirst', 'foo@example.com', 'nickcomesfirst'),
+            ('', 'foo@example.com', 'fooexamplecom'),
+            ('noemail', '', 'noemail'),
+            ('', '@%.-', 'openiduser'),
+            ('', '', 'openiduser'),
+            (None, None, 'openiduser')]:
+            self.assertEqual(expected,
+                self.backend._get_preferred_username(nick, email))
+
+    def test_preferred_username_no_email_munging(self):
+        for nick, email, expected in [
+            ('nickcomesfirst', 'foo@example.com', 'nickcomesfirst'),
+            ('', 'foo@example.com', 'openiduser'),
+            ('noemail', '', 'noemail'),
+            ('', '@%.-', 'openiduser'),
+            ('', '', 'openiduser'),
+            (None, None, 'openiduser')]:
+            self.assertEqual(expected,
+                self.backend._get_preferred_username(nick, email))
+
+
+def suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)

+ 193 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_store.py

@@ -0,0 +1,193 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import time
+import unittest
+
+from django.test import TestCase
+from openid.association import Association as OIDAssociation
+from openid.store.nonce import SKEW
+
+from django_openid_auth.models import Association, Nonce
+from django_openid_auth.store import DjangoOpenIDStore
+
+
+class OpenIDStoreTests(TestCase):
+
+    def setUp(self):
+        super(OpenIDStoreTests, self).setUp()
+        self.store = DjangoOpenIDStore()
+
+    def test_storeAssociation(self):
+        assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1')
+        self.store.storeAssociation('server-url', assoc)
+
+        dbassoc = Association.objects.get(
+            server_url='server-url', handle='handle')
+        self.assertEquals(dbassoc.server_url, 'server-url')
+        self.assertEquals(dbassoc.handle, 'handle')
+        self.assertEquals(dbassoc.secret, 'secret'.encode('base-64'))
+        self.assertEquals(dbassoc.issued, 42)
+        self.assertEquals(dbassoc.lifetime, 600)
+        self.assertEquals(dbassoc.assoc_type, 'HMAC-SHA1')
+
+    def test_storeAssociation_update_existing(self):
+        assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1')
+        self.store.storeAssociation('server-url', assoc)
+
+        # Now update the association with new information.
+        assoc = OIDAssociation('handle', 'secret2', 420, 900, 'HMAC-SHA256')
+        self.store.storeAssociation('server-url', assoc)
+        dbassoc = Association.objects.get(
+            server_url='server-url', handle='handle')
+        self.assertEqual(dbassoc.secret, 'secret2'.encode('base-64'))
+        self.assertEqual(dbassoc.issued, 420)
+        self.assertEqual(dbassoc.lifetime, 900)
+        self.assertEqual(dbassoc.assoc_type, 'HMAC-SHA256')
+
+    def test_getAssociation(self):
+        timestamp = int(time.time())
+        self.store.storeAssociation(
+            'server-url', OIDAssociation('handle', 'secret', timestamp, 600,
+                                         'HMAC-SHA1'))
+        assoc = self.store.getAssociation('server-url', 'handle')
+        self.assertTrue(isinstance(assoc, OIDAssociation))
+
+        self.assertEquals(assoc.handle, 'handle')
+        self.assertEquals(assoc.secret, 'secret')
+        self.assertEquals(assoc.issued, timestamp)
+        self.assertEquals(assoc.lifetime, 600)
+        self.assertEquals(assoc.assoc_type, 'HMAC-SHA1')
+
+    def test_getAssociation_unknown(self):
+        assoc = self.store.getAssociation('server-url', 'unknown')
+        self.assertEquals(assoc, None)
+
+    def test_getAssociation_expired(self):
+        lifetime = 600
+        timestamp = int(time.time()) - 2 * lifetime
+        self.store.storeAssociation(
+            'server-url', OIDAssociation('handle', 'secret', timestamp,
+                                         lifetime, 'HMAC-SHA1'))
+
+        # The association is not returned, and is removed from the database.
+        assoc = self.store.getAssociation('server-url', 'handle')
+        self.assertEquals(assoc, None)
+        self.assertRaises(Association.DoesNotExist, Association.objects.get,
+                          server_url='server-url', handle='handle')
+
+    def test_getAssociation_no_handle(self):
+        timestamp = int(time.time())
+
+        self.store.storeAssociation(
+            'server-url', OIDAssociation('handle1', 'secret', timestamp + 1,
+                                         600, 'HMAC-SHA1'))
+        self.store.storeAssociation(
+            'server-url', OIDAssociation('handle2', 'secret', timestamp,
+                                         600, 'HMAC-SHA1'))
+
+        # The newest handle is returned.
+        assoc = self.store.getAssociation('server-url', None)
+        self.assertNotEquals(assoc, None)
+        self.assertEquals(assoc.handle, 'handle1')
+        self.assertEquals(assoc.issued, timestamp + 1)
+
+    def test_removeAssociation(self):
+        timestamp = int(time.time())
+        self.store.storeAssociation(
+            'server-url', OIDAssociation('handle', 'secret', timestamp, 600,
+                                         'HMAC-SHA1'))
+        self.assertEquals(
+            self.store.removeAssociation('server-url', 'handle'), True)
+        self.assertEquals(
+            self.store.getAssociation('server-url', 'handle'), None)
+
+    def test_removeAssociation_unknown(self):
+        self.assertEquals(
+            self.store.removeAssociation('server-url', 'unknown'), False)
+
+    def test_useNonce(self):
+        timestamp = time.time()
+        # The nonce can only be used once.
+        self.assertEqual(
+            self.store.useNonce('server-url', timestamp, 'salt'), True)
+        self.assertEqual(
+            self.store.useNonce('server-url', timestamp, 'salt'), False)
+        self.assertEqual(
+            self.store.useNonce('server-url', timestamp, 'salt'), False)
+
+    def test_useNonce_expired(self):
+        timestamp = time.time() - 2 * SKEW
+        self.assertEqual(
+            self.store.useNonce('server-url', timestamp, 'salt'), False)
+
+    def test_useNonce_future(self):
+        timestamp = time.time() + 2 * SKEW
+        self.assertEqual(
+            self.store.useNonce('server-url', timestamp, 'salt'), False)
+
+    def test_cleanupNonces(self):
+        timestamp = time.time()
+        self.assertEqual(
+            self.store.useNonce('server1', timestamp, 'salt1'), True)
+        self.assertEqual(
+            self.store.useNonce('server2', timestamp, 'salt2'), True)
+        self.assertEqual(
+            self.store.useNonce('server3', timestamp, 'salt3'), True)
+        self.assertEqual(Nonce.objects.count(), 3)
+
+        self.assertEqual(
+            self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 3)
+        self.assertEqual(Nonce.objects.count(), 0)
+
+        # The nonces have now been cleared:
+        self.assertEqual(
+            self.store.useNonce('server1', timestamp, 'salt1'), True)
+        self.assertEqual(
+            self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 1)
+        self.assertEqual(
+            self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 0)
+
+    def test_cleanupAssociations(self):
+        timestamp = int(time.time()) - 100
+        self.store.storeAssociation(
+            'server-url', OIDAssociation('handle1', 'secret', timestamp,
+                                         50, 'HMAC-SHA1'))
+        self.store.storeAssociation(
+            'server-url', OIDAssociation('handle2', 'secret', timestamp,
+                                         200, 'HMAC-SHA1'))
+
+        self.assertEquals(self.store.cleanupAssociations(), 1)
+
+        # The second (non-expired) association is left behind.
+        self.assertNotEqual(self.store.getAssociation('server-url', 'handle2'),
+                            None)
+
+
+def suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)

+ 1376 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_views.py

@@ -0,0 +1,1376 @@
+# -*- coding: utf-8 -*-
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import cgi
+import unittest
+from urllib import quote_plus
+
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.http import HttpRequest, HttpResponse
+from django.test import TestCase
+from openid.consumer.consumer import Consumer, SuccessResponse
+from openid.consumer.discover import OpenIDServiceEndpoint
+from openid.extensions import ax, sreg, pape
+from openid.fetchers import (
+    HTTPFetcher, HTTPFetchingError, HTTPResponse, setDefaultFetcher)
+from openid.oidutil import importElementTree
+from openid.server.server import BROWSER_REQUEST_MODES, ENCODE_URL, Server
+from openid.store.memstore import MemoryStore
+from openid.message import IDENTIFIER_SELECT
+
+from django_openid_auth import teams
+from django_openid_auth.models import UserOpenID
+from django_openid_auth.views import (
+    sanitise_redirect_url, 
+    make_consumer,
+)
+from django_openid_auth.signals import openid_login_complete
+from django_openid_auth.store import DjangoOpenIDStore
+from django_openid_auth.exceptions import (
+    MissingUsernameViolation,
+    DuplicateUsernameViolation,
+    MissingPhysicalMultiFactor,
+    RequiredAttributeNotReturned,
+)
+
+ET = importElementTree()
+
+class StubOpenIDProvider(HTTPFetcher):
+
+    def __init__(self, base_url):
+        self.store = MemoryStore()
+        self.identity_url = base_url + 'identity'
+        self.localid_url = base_url + 'localid'
+        self.endpoint_url = base_url + 'endpoint'
+        self.server = Server(self.store, self.endpoint_url)
+        self.last_request = None
+        self.type_uris = ['http://specs.openid.net/auth/2.0/signon']
+
+    def fetch(self, url, body=None, headers=None):
+        if url == self.identity_url:
+            # Serve an XRDS document directly, pointing at our endpoint.
+            type_uris = ['<Type>%s</Type>' % uri for uri in self.type_uris]
+            return HTTPResponse(
+                url, 200, {'content-type': 'application/xrds+xml'}, """\
+<?xml version="1.0"?>
+<xrds:XRDS
+    xmlns="xri://$xrd*($v*2.0)"
+    xmlns:xrds="xri://$xrds">
+  <XRD>
+    <Service priority="0">
+      %s
+      <URI>%s</URI>
+      <LocalID>%s</LocalID>
+    </Service>
+  </XRD>
+</xrds:XRDS>
+""" % ('\n'.join(type_uris), self.endpoint_url, self.localid_url))
+        elif url.startswith(self.endpoint_url):
+            # Gather query parameters
+            query = {}
+            if '?' in url:
+                query.update(cgi.parse_qsl(url.split('?', 1)[1]))
+            if body is not None:
+                query.update(cgi.parse_qsl(body))
+            self.last_request = self.server.decodeRequest(query)
+
+            # The browser based requests should not be handled through
+            # the fetcher interface.
+            assert self.last_request.mode not in BROWSER_REQUEST_MODES
+
+            response = self.server.handleRequest(self.last_request)
+            webresponse = self.server.encodeResponse(response)
+            return HTTPResponse(url,  webresponse.code, webresponse.headers,
+                                webresponse.body)
+        else:
+            raise HTTPFetchingError('unknown URL %s' % url)
+
+    def parseFormPost(self, content):
+        """Parse an HTML form post to create an OpenID request."""
+        # Hack to make the javascript XML compliant ...
+        content = content.replace('i < elements.length',
+                                  'i &lt; elements.length')
+        tree = ET.XML(content)
+        form = tree.find('.//form')
+        assert form is not None, 'No form in document'
+        assert form.get('action') == self.endpoint_url, (
+            'Form posts to %s instead of %s' % (form.get('action'),
+                                                self.endpoint_url))
+        query = {}
+        for input in form.findall('input'):
+            if input.get('type') != 'hidden':
+                continue
+            query[input.get('name').encode('UTF-8')] = \
+                input.get('value').encode('UTF-8')
+        self.last_request = self.server.decodeRequest(query)
+        return self.last_request
+
+
+class DummyDjangoRequest(object):
+    def __init__(self, request_path):
+        self.request_path = request_path
+        self.META = {
+            'HTTP_HOST': "localhost",
+            'SCRIPT_NAME': "http://localhost",
+            'SERVER_PROTOCOL': "http",
+        }
+        self.POST = {
+            'openid_identifier': "http://example.com/identity",
+        }
+        self.GET = {}
+        self.session = {}
+
+    def get_full_path(self):
+        return self.META['SCRIPT_NAME'] + self.request_path
+
+    def build_absolute_uri(self):
+        return self.META['SCRIPT_NAME'] + self.request_path
+        
+    def _combined_request(self):
+        request = {}
+        request.update(self.POST)
+        request.update(self.GET)
+        return request
+    REQUEST = property(_combined_request)
+
+class RelyingPartyTests(TestCase):
+    urls = 'django_openid_auth.tests.urls'
+
+    def setUp(self):
+        super(RelyingPartyTests, self).setUp()
+        self.provider = StubOpenIDProvider('http://example.com/')
+        self.req = DummyDjangoRequest('http://localhost/')
+        self.endpoint = OpenIDServiceEndpoint()
+        self.endpoint.claimed_id = 'http://example.com/identity'
+        server_url = 'http://example.com/'
+        self.endpoint.server_url = server_url
+        self.consumer = make_consumer(self.req)
+        self.server = Server(DjangoOpenIDStore(), op_endpoint=server_url)
+        setDefaultFetcher(self.provider, wrap_exceptions=False)
+
+        self.old_login_redirect_url = getattr(settings, 'LOGIN_REDIRECT_URL', '/accounts/profile/')
+        self.old_create_users = getattr(settings, 'OPENID_CREATE_USERS', False)
+        self.old_strict_usernames = getattr(settings, 'OPENID_STRICT_USERNAMES', False)
+        self.old_update_details = getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False)
+        self.old_sso_server_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None)
+        self.old_teams_map = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+        self.old_use_as_admin_login = getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False)
+        self.old_follow_renames = getattr(settings, 'OPENID_FOLLOW_RENAMES', False)
+        self.old_physical_multifactor = getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False)
+        self.old_login_render_failure = getattr(settings, 'OPENID_RENDER_FAILURE', None)
+        self.old_consumer_complete = Consumer.complete
+        self.old_openid_use_email_for_username = getattr(settings,
+            'OPENID_USE_EMAIL_FOR_USERNAME', False)
+
+        self.old_required_fields = getattr(
+            settings, 'OPENID_SREG_REQUIRED_FIELDS', [])
+
+        settings.OPENID_CREATE_USERS = False
+        settings.OPENID_STRICT_USERNAMES = False
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = False
+        settings.OPENID_SSO_SERVER_URL = None
+        settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {}
+        settings.OPENID_USE_AS_ADMIN_LOGIN = False
+        settings.OPENID_FOLLOW_RENAMES = False
+        settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = False
+        settings.OPENID_SREG_REQUIRED_FIELDS = []
+        settings.OPENID_USE_EMAIL_FOR_USERNAME = False
+
+    def tearDown(self):
+        settings.LOGIN_REDIRECT_URL = self.old_login_redirect_url
+        settings.OPENID_CREATE_USERS = self.old_create_users
+        settings.OPENID_STRICT_USERNAMES = self.old_strict_usernames
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = self.old_update_details
+        settings.OPENID_SSO_SERVER_URL = self.old_sso_server_url
+        settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = self.old_teams_map
+        settings.OPENID_USE_AS_ADMIN_LOGIN = self.old_use_as_admin_login
+        settings.OPENID_FOLLOW_RENAMES = self.old_follow_renames
+        settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = self.old_physical_multifactor
+        settings.OPENID_RENDER_FAILURE = self.old_login_render_failure
+        Consumer.complete = self.old_consumer_complete
+        settings.OPENID_SREG_REQUIRED_FIELDS = self.old_required_fields
+        settings.OPENID_USE_EMAIL_FOR_USERNAME = self.old_openid_use_email_for_username
+
+        setDefaultFetcher(None)
+        super(RelyingPartyTests, self).tearDown()
+
+    def complete(self, openid_response):
+        """Complete an OpenID authentication request."""
+        # The server can generate either a redirect or a form post
+        # here.  For simplicity, force generation of a redirect.
+        openid_response.whichEncoding = lambda: ENCODE_URL
+        webresponse = self.provider.server.encodeResponse(openid_response)
+        self.assertEquals(webresponse.code, 302)
+        redirect_to = webresponse.headers['location']
+        self.assertTrue(redirect_to.startswith(
+                'http://testserver/openid/complete/'))
+        return self.client.get('/openid/complete/',
+            dict(cgi.parse_qsl(redirect_to.split('?', 1)[1])))
+
+    def test_login(self):
+        user = User.objects.create_user('someuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # The login form is displayed:
+        response = self.client.get('/openid/login/')
+        self.assertTemplateUsed(response, 'openid/login.html')
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        openid_request = self.provider.parseFormPost(response.content)
+        self.assertEquals(openid_request.mode, 'checkid_setup')
+        self.assertTrue(openid_request.return_to.startswith(
+                'http://testserver/openid/complete/'))
+
+        # Complete the request.  The user is redirected to the next URL.
+        openid_response = openid_request.answer(True)
+        response = self.complete(openid_response)
+        self.assertRedirects(response, 'http://testserver/getuser/')
+
+        # And they are now logged in:
+        response = self.client.get('/getuser/')
+        self.assertEquals(response.content, 'someuser')
+
+    def test_login_with_nonascii_return_to(self):
+        """Ensure non-ascii characters can be used for the 'next' arg."""
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': u'/files/ñandú.jpg'.encode('utf-8')})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+    def test_login_no_next(self):
+        """Logins with no next parameter redirect to LOGIN_REDIRECT_URL."""
+        user = User.objects.create_user('someuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        settings.LOGIN_REDIRECT_URL = '/getuser/'
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        openid_request = self.provider.parseFormPost(response.content)
+        self.assertEquals(openid_request.mode, 'checkid_setup')
+        self.assertTrue(openid_request.return_to.startswith(
+                'http://testserver/openid/complete/'))
+
+        # Complete the request.  The user is redirected to the next URL.
+        openid_response = openid_request.answer(True)
+        response = self.complete(openid_response)
+        self.assertRedirects(
+            response, 'http://testserver' + settings.LOGIN_REDIRECT_URL)
+
+    def test_login_sso(self):
+        settings.OPENID_SSO_SERVER_URL = 'http://example.com/identity'
+        user = User.objects.create_user('someuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Requesting the login form immediately begins an
+        # authentication request.
+        response = self.client.get('/openid/login/', {'next': '/getuser/'})
+        self.assertEquals(response.status_code, 200)
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        openid_request = self.provider.parseFormPost(response.content)
+        self.assertEquals(openid_request.mode, 'checkid_setup')
+        self.assertTrue(openid_request.return_to.startswith(
+                'http://testserver/openid/complete/'))
+
+        # Complete the request.  The user is redirected to the next URL.
+        openid_response = openid_request.answer(True)
+        response = self.complete(openid_response)
+        self.assertRedirects(response, 'http://testserver/getuser/')
+
+        # And they are now logged in:
+        response = self.client.get('/getuser/')
+        self.assertEquals(response.content, 'someuser')
+
+    def test_login_create_users(self):
+        settings.OPENID_CREATE_USERS = True
+        # Create a user with the same name as we'll pass back via sreg.
+        User.objects.create_user('someuser', 'someone@example.com')
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request, passing back some simple registration
+        # data.  The user is redirected to the next URL.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        openid_response = openid_request.answer(True)
+        sreg_response = sreg.SRegResponse.extractResponse(
+            sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
+                           'email': 'foo@example.com'})
+        openid_response.addExtension(sreg_response)
+        response = self.complete(openid_response)
+        self.assertRedirects(response, 'http://testserver/getuser/')
+
+        # And they are now logged in as a new user (they haven't taken
+        # over the existing "someuser" user).
+        response = self.client.get('/getuser/')
+        self.assertEquals(response.content, 'someuser2')
+
+        # Check the details of the new user.
+        user = User.objects.get(username='someuser2')
+        self.assertEquals(user.first_name, 'Some')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'foo@example.com')
+
+    def _do_user_login(self, req_data, resp_data, use_sreg=True, use_pape=None):
+        openid_request = self._get_login_request(req_data)
+        openid_response = self._get_login_response(openid_request, resp_data, use_sreg, use_pape)
+        response = self.complete(openid_response)
+        self.assertRedirects(response, 'http://testserver/getuser/')
+        return response
+
+    def _get_login_request(self, req_data):
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/', req_data)
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request, passing back some simple registration
+        # data.  The user is redirected to the next URL.
+        openid_request = self.provider.parseFormPost(response.content)
+        return openid_request
+
+    def _get_login_response(self, openid_request, resp_data, use_sreg, use_pape):
+        openid_response = openid_request.answer(True)
+
+        if use_sreg:
+            sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+            sreg_response = sreg.SRegResponse.extractResponse(
+                sreg_request, resp_data)
+            openid_response.addExtension(sreg_response)
+        if use_pape is not None:
+            policies = [
+                use_pape
+            ]
+            pape_response = pape.Response(auth_policies=policies)
+            openid_response.addExtension(pape_response)
+        return openid_response
+
+    def parse_query_string(self, query_str):
+        query_items = map(tuple,
+            [item.split('=') for item in query_str.split('&')])
+        query = dict(query_items)
+        return query
+
+    def test_login_physical_multifactor_request(self):
+        settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True
+        preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
+        self.provider.type_uris.append(pape.ns_uri)
+        
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        response = self.client.post('/openid/login/', openid_req)
+        openid_request = self.provider.parseFormPost(response.content)
+
+        request_auth = openid_request.message.getArg(
+            'http://specs.openid.net/extensions/pape/1.0',
+            'preferred_auth_policies',
+        )
+        self.assertEqual(request_auth, preferred_auth)
+
+    def test_login_physical_multifactor_response(self):
+        settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True
+        preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
+        self.provider.type_uris.append(pape.ns_uri)
+
+        def mock_complete(this, request_args, return_to):
+            request = {'openid.mode': 'checkid_setup',
+                       'openid.trust_root': 'http://localhost/',
+                       'openid.return_to': 'http://localhost/',
+                       'openid.identity': IDENTIFIER_SELECT,
+                       'openid.ns.pape' : pape.ns_uri,
+                       'openid.pape.auth_policies': request_args.get('openid.pape.auth_policies', pape.AUTH_NONE),
+            }
+            openid_server = self.provider.server
+            orequest = openid_server.decodeRequest(request)
+            response = SuccessResponse(
+                self.endpoint, orequest.message,
+                signed_fields=['openid.pape.auth_policies',])
+            return response
+        Consumer.complete = mock_complete
+
+        user = User.objects.create_user('testuser', 'test@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Openid User',
+                 'email': 'test@example.com'}
+
+        response = self._do_user_login(openid_req, openid_resp, use_pape=pape.AUTH_MULTI_FACTOR_PHYSICAL)
+
+        query = self.parse_query_string(response.request['QUERY_STRING'])
+        self.assertTrue('openid.pape.auth_policies' in query)
+        self.assertEqual(query['openid.pape.auth_policies'], 
+                quote_plus(preferred_auth))
+
+        response = self.client.get('/getuser/')
+        self.assertEqual(response.content, 'testuser')
+
+
+    def test_login_physical_multifactor_not_provided(self):
+        settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True
+        preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
+        self.provider.type_uris.append(pape.ns_uri)
+
+        def mock_complete(this, request_args, return_to):
+            request = {'openid.mode': 'checkid_setup',
+                       'openid.trust_root': 'http://localhost/',
+                       'openid.return_to': 'http://localhost/',
+                       'openid.identity': IDENTIFIER_SELECT,
+                       'openid.ns.pape' : pape.ns_uri,
+                       'openid.pape.auth_policies': request_args.get('openid.pape.auth_policies', pape.AUTH_NONE),
+            }
+            openid_server = self.provider.server
+            orequest = openid_server.decodeRequest(request)
+            response = SuccessResponse(
+                self.endpoint, orequest.message,
+                signed_fields=['openid.pape.auth_policies',])
+            return response
+        Consumer.complete = mock_complete
+
+        user = User.objects.create_user('testuser', 'test@example.com')
+        useropenid = UserOpenID(    
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Openid User',
+                 'email': 'test@example.com'}
+
+        openid_request = self._get_login_request(openid_req)
+        openid_response = self._get_login_response(openid_request, openid_req, openid_resp, use_pape=pape.AUTH_NONE)
+
+        response_auth = openid_request.message.getArg(
+            'http://specs.openid.net/extensions/pape/1.0',
+            'auth_policies',
+        )
+        self.assertNotEqual(response_auth, preferred_auth)
+
+        response = self.complete(openid_response)
+        self.assertEquals(403, response.status_code)
+        self.assertContains(response, '<h1>OpenID failed</h1>', status_code=403)
+        self.assertContains(response, '<p>Login requires physical multi-factor authentication.</p>', status_code=403)
+
+    def test_login_physical_multifactor_not_provided_override(self):
+        settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True
+        preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
+        self.provider.type_uris.append(pape.ns_uri)
+
+        # Override the login_failure handler
+        def mock_login_failure_handler(request, message, status=403,
+                                       template_name=None,
+                                       exception=None):
+           self.assertTrue(isinstance(exception, MissingPhysicalMultiFactor))
+           return HttpResponse('Test Failure Override', status=200)
+        settings.OPENID_RENDER_FAILURE = mock_login_failure_handler
+
+        def mock_complete(this, request_args, return_to):
+            request = {'openid.mode': 'checkid_setup',
+                       'openid.trust_root': 'http://localhost/',
+                       'openid.return_to': 'http://localhost/',
+                       'openid.identity': IDENTIFIER_SELECT,
+                       'openid.ns.pape' : pape.ns_uri,
+                       'openid.pape.auth_policies': request_args.get('openid.pape.auth_policies', pape.AUTH_NONE),
+            }
+            openid_server = self.provider.server
+            orequest = openid_server.decodeRequest(request)
+            response = SuccessResponse(
+                self.endpoint, orequest.message,
+                signed_fields=['openid.pape.auth_policies',])
+            return response
+        Consumer.complete = mock_complete
+
+        user = User.objects.create_user('testuser', 'test@example.com')
+        useropenid = UserOpenID(    
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Openid User',
+                 'email': 'test@example.com'}
+
+        openid_request = self._get_login_request(openid_req)
+        openid_response = self._get_login_response(openid_request, openid_req, openid_resp, use_pape=pape.AUTH_NONE)
+
+        response_auth = openid_request.message.getArg(
+            'http://specs.openid.net/extensions/pape/1.0',
+            'auth_policies',
+        )
+        self.assertNotEqual(response_auth, preferred_auth)
+
+        # Status code should be 200, since we over-rode the login_failure handler
+        response = self.complete(openid_response)
+        self.assertEquals(200, response.status_code)
+        self.assertContains(response, 'Test Failure Override')
+
+    def test_login_without_nickname(self):
+        settings.OPENID_CREATE_USERS = True
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': '', 'fullname': 'Openid User',
+                 'email': 'foo@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # username defaults to 'openiduser'
+        self.assertEquals(response.content, 'openiduser')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Openid')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'foo@example.com')
+
+    def test_login_without_nickname_with_email_suggestion(self):
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_USE_EMAIL_FOR_USERNAME = True
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': '', 'fullname': 'Openid User',
+                 'email': 'foo@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # username defaults to a munged version of the email
+        self.assertEquals(response.content, 'fooexamplecom')
+
+    def test_login_duplicate_username_numbering(self):
+        settings.OPENID_FOLLOW_RENAMES = False
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's name we're going to conflict with
+        user = User.objects.create_user('testuser', 'someone@example.com')
+
+        # identity url is for 'renameuser'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which already exists for another identity
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Test User',
+                 'email': 'test@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # Since this username is already taken by someone else, we go through
+        # the process of adding +i to it, and get testuser2.
+        self.assertEquals(response.content, 'testuser2')
+
+    def test_login_duplicate_username_numbering_with_conflicts(self):
+        settings.OPENID_FOLLOW_RENAMES = False
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's name we're going to conflict with
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        user = User.objects.create_user('testuser3', 'someone@example.com')
+
+        # identity url is for 'renameuser'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which already exists for another identity
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Test User',
+                 'email': 'test@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # Since this username is already taken by someone else, we go through
+        # the process of adding +i to it starting with the count of users with
+        # username starting with 'testuser', of which there are 2.  i should
+        # start at 3, which already exists, so it should skip to 4.
+        self.assertEquals(response.content, 'testuser4')
+
+    def test_login_duplicate_username_numbering_with_holes(self):
+        settings.OPENID_FOLLOW_RENAMES = False
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's name we're going to conflict with
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        user = User.objects.create_user('testuser1', 'someone@example.com')
+        user = User.objects.create_user('testuser6', 'someone@example.com')
+        user = User.objects.create_user('testuser7', 'someone@example.com')
+        user = User.objects.create_user('testuser8', 'someone@example.com')
+
+        # identity url is for 'renameuser'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which already exists for another identity
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Test User',
+                 'email': 'test@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # Since this username is already taken by someone else, we go through
+        # the process of adding +i to it starting with the count of users with
+        # username starting with 'testuser', of which there are 5.  i should
+        # start at 6, and increment until it reaches 9.
+        self.assertEquals(response.content, 'testuser9')
+
+    def test_login_duplicate_username_numbering_with_nonsequential_matches(self):
+        settings.OPENID_FOLLOW_RENAMES = False
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's name we're going to conflict with
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        user = User.objects.create_user('testuserfoo', 'someone@example.com')
+
+        # identity url is for 'renameuser'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which already exists for another identity
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Test User',
+                 'email': 'test@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # Since this username is already taken by someone else, we go through
+        # the process of adding +i to it starting with the count of users with
+        # username starting with 'testuser', of which there are 2.  i should
+        # start at 3, which will be available.
+        self.assertEquals(response.content, 'testuser3')
+
+    def test_login_follow_rename(self):
+        settings.OPENID_FOLLOW_RENAMES = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': 'someuser', 'fullname': 'Some User',
+                 'email': 'foo@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # If OPENID_FOLLOW_RENAMES, they are logged in as
+        # someuser (the passed in nickname has changed the username)
+        self.assertEquals(response.content, 'someuser')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Some')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'foo@example.com')
+
+    def test_login_follow_rename_without_nickname_change(self):
+        settings.OPENID_FOLLOW_RENAMES = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        settings.OPENID_STRICT_USERNAMES = True
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Some User',
+                 'email': 'foo@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # Username should not have changed
+        self.assertEquals(response.content, 'testuser')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Some')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'foo@example.com')
+
+    def test_login_follow_rename_conflict(self):
+        settings.OPENID_FOLLOW_RENAMES = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's name we're going to switch to
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        UserOpenID.objects.get_or_create(
+            user=user,
+            claimed_id='http://example.com/existing_identity',
+            display_id='http://example.com/existing_identity')
+
+        # Setup user who is going to try to change username to 'testuser'
+        renamed_user = User.objects.create_user('renameuser', 'someone@example.com')
+        UserOpenID.objects.get_or_create(
+            user=renamed_user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+
+        # identity url is for 'renameuser'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which already exists for another identity
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Rename User',
+                 'email': 'rename@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser'
+        # but since that username is already taken by someone else, we go through
+        # the process of adding +i to it, and get testuser2.
+        self.assertEquals(response.content, 'testuser2')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Rename')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'rename@example.com')
+
+    def test_login_follow_rename_false_onlyonce(self):
+        settings.OPENID_FOLLOW_RENAMES = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's name we're going to switch to
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        UserOpenID.objects.get_or_create(
+            user=user,
+            claimed_id='http://example.com/existing_identity',
+            display_id='http://example.com/existing_identity')
+
+        # Setup user who is going to try to change username to 'testuser'
+        renamed_user = User.objects.create_user('testuser2000eight', 'someone@example.com')
+        UserOpenID.objects.get_or_create(
+            user=renamed_user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+
+        # identity url is for 'testuser2000eight'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which already exists for another identity
+        openid_resp =  {'nickname': 'testuser2', 'fullname': 'Rename User',
+                 'email': 'rename@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser'
+        # but since that username is already taken by someone else, we go through
+        # the process of adding +i to it.  Even though it looks like the username
+        # follows the nickname+i scheme, it has non-numbers in the suffix, so
+        # it's not an auto-generated one.  The regular process of renaming to
+        # 'testuser' has a conflict, so we get +2 at the end.
+        self.assertEquals(response.content, 'testuser2')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Rename')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'rename@example.com')
+
+    def test_login_follow_rename_conflict_onlyonce(self):
+        settings.OPENID_FOLLOW_RENAMES = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's name we're going to switch to
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        UserOpenID.objects.get_or_create(
+            user=user,
+            claimed_id='http://example.com/existing_identity',
+            display_id='http://example.com/existing_identity')
+
+        # Setup user who is going to try to change username to 'testuser'
+        renamed_user = User.objects.create_user('testuser2000', 'someone@example.com')
+        UserOpenID.objects.get_or_create(
+            user=renamed_user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+
+        # identity url is for 'testuser2000'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which already exists for another identity
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Rename User',
+                 'email': 'rename@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser'
+        # but since that username is already taken by someone else, we go through
+        # the process of adding +i to it.  Since the user for this identity url
+        # already has a name matching that pattern, check if first.
+        self.assertEquals(response.content, 'testuser2000')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Rename')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'rename@example.com')
+
+    def test_login_follow_rename_false_conflict(self):
+        settings.OPENID_FOLLOW_RENAMES = True
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        # Setup existing user who's username matches the name+i pattern
+        user = User.objects.create_user('testuser2', 'someone@example.com')
+        UserOpenID.objects.get_or_create(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+
+        # identity url is for 'testuser2'
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        # but returned username is for 'testuser', which looks like we've done
+        # a username+1 for them already, but 'testuser' isn't actually taken
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Same User',
+                 'email': 'same@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        # If OPENID_FOLLOW_RENAMES, username should be changed to 'testuser'
+        # because it wasn't currently taken
+        self.assertEquals(response.content, 'testuser')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Same')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'same@example.com')
+
+    def test_strict_username_no_nickname(self):
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_STRICT_USERNAMES = True
+        settings.OPENID_SREG_REQUIRED_FIELDS = []
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request, passing back some simple registration
+        # data.  The user is redirected to the next URL.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        openid_response = openid_request.answer(True)
+        sreg_response = sreg.SRegResponse.extractResponse(
+            sreg_request, {'nickname': '', # No nickname
+                           'fullname': 'Some User',
+                           'email': 'foo@example.com'})
+        openid_response.addExtension(sreg_response)
+        response = self.complete(openid_response)
+
+        # Status code should be 403: Forbidden
+        self.assertEquals(403, response.status_code)
+        self.assertContains(response, '<h1>OpenID failed</h1>', status_code=403)
+        self.assertContains(response, "An attribute required for logging in was not returned "
+            "(nickname)", status_code=403)
+
+    def test_strict_username_no_nickname_override(self):
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_STRICT_USERNAMES = True
+        settings.OPENID_SREG_REQUIRED_FIELDS = []
+
+        # Override the login_failure handler
+        def mock_login_failure_handler(request, message, status=403,
+                                       template_name=None,
+                                       exception=None):
+           self.assertTrue(isinstance(exception, (RequiredAttributeNotReturned, MissingUsernameViolation)))
+           return HttpResponse('Test Failure Override', status=200)
+        settings.OPENID_RENDER_FAILURE = mock_login_failure_handler
+        
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request, passing back some simple registration
+        # data.  The user is redirected to the next URL.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        openid_response = openid_request.answer(True)
+        sreg_response = sreg.SRegResponse.extractResponse(
+            sreg_request, {'nickname': '', # No nickname
+                           'fullname': 'Some User',
+                           'email': 'foo@example.com'})
+        openid_response.addExtension(sreg_response)
+        response = self.complete(openid_response)
+            
+        # Status code should be 200, since we over-rode the login_failure handler
+        self.assertEquals(200, response.status_code)
+        self.assertContains(response, 'Test Failure Override')
+
+    def test_strict_username_duplicate_user(self):
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_STRICT_USERNAMES = True
+        # Create a user with the same name as we'll pass back via sreg.
+        user = User.objects.create_user('someuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/different_identity',
+            display_id='http://example.com/different_identity')
+        useropenid.save()
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request, passing back some simple registration
+        # data.  The user is redirected to the next URL.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        openid_response = openid_request.answer(True)
+        sreg_response = sreg.SRegResponse.extractResponse(
+            sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
+                           'email': 'foo@example.com'})
+        openid_response.addExtension(sreg_response)
+        response = self.complete(openid_response)
+
+        # Status code should be 403: Forbidden
+        self.assertEquals(403, response.status_code)
+        self.assertContains(response, '<h1>OpenID failed</h1>', status_code=403)
+        self.assertContains(response,
+            "The username (someuser) with which you tried to log in is "
+            "already in use for a different account.",
+            status_code=403)
+
+    def test_strict_username_duplicate_user_override(self):
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_STRICT_USERNAMES = True
+
+        # Override the login_failure handler
+        def mock_login_failure_handler(request, message, status=403,
+                                       template_name=None,
+                                       exception=None):
+           self.assertTrue(isinstance(exception, DuplicateUsernameViolation))
+           return HttpResponse('Test Failure Override', status=200)
+        settings.OPENID_RENDER_FAILURE = mock_login_failure_handler
+
+        # Create a user with the same name as we'll pass back via sreg.
+        user = User.objects.create_user('someuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/different_identity',
+            display_id='http://example.com/different_identity')
+        useropenid.save()
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request, passing back some simple registration
+        # data.  The user is redirected to the next URL.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        openid_response = openid_request.answer(True)
+        sreg_response = sreg.SRegResponse.extractResponse(
+            sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
+                           'email': 'foo@example.com'})
+        openid_response.addExtension(sreg_response)
+        response = self.complete(openid_response)
+        
+        # Status code should be 200, since we over-rode the login_failure handler
+        self.assertEquals(200, response.status_code)
+        self.assertContains(response, 'Test Failure Override')
+
+    def test_login_requires_sreg_required_fields(self):
+        # If any required attributes are not included in the response,
+        # we fail with a forbidden.
+        settings.OPENID_CREATE_USERS = True
+        settings.OPENID_SREG_REQUIRED_FIELDS = ('email', 'language')
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request, passing back some simple registration
+        # data.  The user is redirected to the next URL.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        openid_response = openid_request.answer(True)
+        sreg_response = sreg.SRegResponse.extractResponse(
+            sreg_request, {'nickname': 'foo',
+                           'fullname': 'Some User',
+                           'email': 'foo@example.com'})
+        openid_response.addExtension(sreg_response)
+        response = self.complete(openid_response)
+
+        # Status code should be 403: Forbidden as we didn't include
+        # a required field - language.
+        self.assertContains(response,
+            "An attribute required for logging in was not returned "
+            "(language)", status_code=403)
+
+    def test_login_update_details(self):
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        openid_req = {'openid_identifier': 'http://example.com/identity',
+               'next': '/getuser/'}
+        openid_resp =  {'nickname': 'testuser', 'fullname': 'Some User',
+                 'email': 'foo@example.com'}
+        self._do_user_login(openid_req, openid_resp)
+        response = self.client.get('/getuser/')
+
+        self.assertEquals(response.content, 'testuser')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username=response.content)
+        self.assertEquals(user.first_name, 'Some')
+        self.assertEquals(user.last_name, 'User')
+        self.assertEquals(user.email, 'foo@example.com')
+
+    def test_login_uses_sreg_extra_fields(self):
+        # The configurable sreg attributes are used in the request.
+        settings.OPENID_SREG_EXTRA_FIELDS = ('language',)
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        for field in ('email', 'fullname', 'nickname', 'language'):
+            self.assertTrue(field in sreg_request)
+
+    def test_login_uses_sreg_required_fields(self):
+        # The configurable sreg attributes are used in the request.
+        settings.OPENID_SREG_REQUIRED_FIELDS = ('email', 'language')
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+
+        self.assertEqual(['email', 'language'], sreg_request.required)
+        self.assertEqual(['fullname', 'nickname'], sreg_request.optional)
+
+    def test_login_attribute_exchange(self):
+        settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Configure the provider to advertise attribute exchange
+        # protocol and start the authentication process:
+        self.provider.type_uris.append('http://openid.net/srv/ax/1.0')
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # The resulting OpenID request uses the Attribute Exchange
+        # extension rather than the Simple Registration extension.
+        openid_request = self.provider.parseFormPost(response.content)
+        sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        self.assertEqual(sreg_request.required, [])
+        self.assertEqual(sreg_request.optional, [])
+
+        fetch_request = ax.FetchRequest.fromOpenIDRequest(openid_request)
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/contact/email'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson/first'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson/last'))
+        self.assertTrue(fetch_request.has_key(
+                'http://axschema.org/namePerson/friendly'))
+        # myOpenID compatibilty attributes:
+        self.assertTrue(fetch_request.has_key(
+                'http://schema.openid.net/contact/email'))
+        self.assertTrue(fetch_request.has_key(
+                'http://schema.openid.net/namePerson'))
+        self.assertTrue(fetch_request.has_key(
+                'http://schema.openid.net/namePerson/friendly'))
+
+        # Build up a response including AX data.
+        openid_response = openid_request.answer(True)
+        fetch_response = ax.FetchResponse(fetch_request)
+        fetch_response.addValue(
+            'http://axschema.org/contact/email', 'foo@example.com')
+        fetch_response.addValue(
+            'http://axschema.org/namePerson/first', 'Firstname')
+        fetch_response.addValue(
+            'http://axschema.org/namePerson/last', 'Lastname')
+        fetch_response.addValue(
+            'http://axschema.org/namePerson/friendly', 'someuser')
+        openid_response.addExtension(fetch_response)
+        response = self.complete(openid_response)
+        self.assertRedirects(response, 'http://testserver/getuser/')
+
+        # And they are now logged in as testuser (the passed in
+        # nickname has not caused the username to change).
+        response = self.client.get('/getuser/')
+        self.assertEquals(response.content, 'testuser')
+
+        # The user's full name and email have been updated.
+        user = User.objects.get(username='testuser')
+        self.assertEquals(user.first_name, 'Firstname')
+        self.assertEquals(user.last_name, 'Lastname')
+        self.assertEquals(user.email, 'foo@example.com')
+
+    def test_login_teams(self):
+        settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = False
+        settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname',
+                                                   'otherteam': 'othergroup'}
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        group = Group(name='groupname')
+        group.save()
+        ogroup = Group(name='othergroup')
+        ogroup.save()
+        user.groups.add(ogroup)
+        user.save()
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request
+        openid_request = self.provider.parseFormPost(response.content)
+        openid_response = openid_request.answer(True)
+        teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request)
+        teams_response = teams.TeamsResponse.extractResponse(
+            teams_request, 'teamname,some-other-team')
+        openid_response.addExtension(teams_response)
+        response = self.complete(openid_response)
+        self.assertRedirects(response, 'http://testserver/getuser/')
+
+        # And they are now logged in as testuser
+        response = self.client.get('/getuser/')
+        self.assertEquals(response.content, 'testuser')
+
+        # The user's groups have been updated.
+        user = User.objects.get(username='testuser')
+        self.assertTrue(group in user.groups.all())
+        self.assertTrue(ogroup not in user.groups.all())
+
+    def test_login_teams_automapping(self):
+        settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname',
+                                                   'otherteam': 'othergroup'}
+        settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True
+        settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2']
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        group1 = Group(name='django-group1')
+        group1.save()
+        group2 = Group(name='django-group2')
+        group2.save()
+        group3 = Group(name='django-group3')
+        group3.save()
+        user.save()
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity',
+             'next': '/getuser/'})
+        self.assertContains(response, 'OpenID transaction in progress')
+
+        # Complete the request
+        openid_request = self.provider.parseFormPost(response.content)
+        openid_response = openid_request.answer(True)
+        teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request)
+
+        self.assertEqual(group1 in user.groups.all(), False)
+        self.assertEqual(group2 in user.groups.all(), False)
+        self.assertTrue(group3 not in user.groups.all())
+
+    def test_login_teams_staff_not_defined(self):
+        delattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS')
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        user.is_staff = True
+        user.save()
+        self.assertTrue(user.is_staff)
+
+        user = self.get_openid_authed_user_with_teams(user, 'teamname,some-other-team')
+        self.assertTrue(user.is_staff)
+
+    def test_login_teams_staff_assignment(self):
+        settings.OPENID_LAUNCHPAD_STAFF_TEAMS = ('teamname',)
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        user.is_staff = False
+        user.save()
+        self.assertFalse(user.is_staff)
+
+        user = self.get_openid_authed_user_with_teams(user, 'teamname,some-other-team')
+        self.assertTrue(user.is_staff)
+
+    def test_login_teams_staff_unassignment(self):
+        settings.OPENID_LAUNCHPAD_STAFF_TEAMS = ('different-teamname',)
+        user = User.objects.create_user('testuser', 'someone@example.com')
+        user.is_staff = True
+        user.save()
+        self.assertTrue(user.is_staff)
+
+        user = self.get_openid_authed_user_with_teams(user, 'teamname,some-other-team')
+        self.assertFalse(user.is_staff)
+
+    def get_openid_authed_user_with_teams(self, user, teams_str):
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+
+        # Posting in an identity URL begins the authentication request:
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity'})
+
+        # Complete the request
+        openid_request = self.provider.parseFormPost(response.content)
+        openid_response = openid_request.answer(True)
+        teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request)
+        teams_response = teams.TeamsResponse.extractResponse(
+            teams_request, teams_str)
+        openid_response.addExtension(teams_response)
+        response = self.complete(openid_response)
+        return User.objects.get(username=user.username)
+
+    def test_login_complete_signals_login(self):
+        # An oauth_login_complete signal is emitted including the
+        # request and sreg_response.
+        user = User.objects.create_user('someuser', 'someone@example.com')
+        useropenid = UserOpenID(
+            user=user,
+            claimed_id='http://example.com/identity',
+            display_id='http://example.com/identity')
+        useropenid.save()
+        response = self.client.post('/openid/login/',
+            {'openid_identifier': 'http://example.com/identity'})
+        openid_request = self.provider.parseFormPost(response.content)
+        openid_response = openid_request.answer(True)
+        # Use a closure to test whether the signal handler was called.
+        self.signal_handler_called = False
+        def login_callback(sender, **kwargs):
+            self.assertTrue(isinstance(
+                kwargs.get('request', None), HttpRequest))
+            self.assertTrue(isinstance(
+                kwargs.get('openid_response', None), SuccessResponse))
+            self.signal_handler_called = True
+        openid_login_complete.connect(login_callback)
+
+        response = self.complete(openid_response)
+
+        self.assertTrue(self.signal_handler_called)
+        openid_login_complete.disconnect(login_callback)
+
+    
+class HelperFunctionsTest(TestCase):
+    def test_sanitise_redirect_url(self):
+        settings.ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
+            "example.com", "example.org"]
+        # list of URLs and whether they should be passed or not
+        urls = [
+            ("http://example.com", True),
+            ("http://example.org/", True),
+            ("http://example.org/foo/bar", True),
+            ("http://example.org/foo/bar?baz=quux", True),
+            ("http://example.org:9999/foo/bar?baz=quux", True),
+            ("http://www.example.org/", False),
+            ("http://example.net/foo/bar?baz=quux", False),
+            ("/somewhere/local", True),
+            ("/somewhere/local?url=http://fail.com/bar", True),
+            # An empty path, as seen when no "next" parameter is passed.
+            ("", False),
+            ("/path with spaces", False),
+        ]
+        for url, returns_self in urls:
+            sanitised = sanitise_redirect_url(url)
+            if returns_self:
+                self.assertEqual(url, sanitised)
+            else:
+                self.assertEqual(settings.LOGIN_REDIRECT_URL, sanitised)
+
+def suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)

+ 39 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/urls.py

@@ -0,0 +1,39 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.http import HttpResponse
+from django.conf.urls.defaults import *
+
+
+def get_user(request):
+    return HttpResponse(request.user.username)
+
+urlpatterns = patterns('',
+    (r'^getuser/$', get_user),
+    (r'^openid/', include('django_openid_auth.urls')),
+)

+ 36 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/urls.py

@@ -0,0 +1,36 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django_openid_auth.views',
+    url(r'^login/$', 'login_begin', name='openid-login'),
+    url(r'^complete/$', 'login_complete', name='openid-complete'),
+    url(r'^logo.gif$', 'logo', name='openid-logo'),
+)

+ 313 - 0
desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/views.py

@@ -0,0 +1,313 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import urllib
+from urlparse import urlsplit
+
+from django.conf import settings
+from django.contrib.auth import (
+    REDIRECT_FIELD_NAME, authenticate, login as auth_login)
+from django.contrib.auth.models import Group
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.template.loader import render_to_string
+try:
+    from django.views.decorators.csrf import csrf_exempt
+except ImportError:
+    from django.contrib.csrf.middleware import csrf_exempt
+
+from openid.consumer.consumer import (
+    Consumer, SUCCESS, CANCEL, FAILURE)
+from openid.consumer.discover import DiscoveryFailure
+from openid.extensions import sreg, ax, pape
+
+from django_openid_auth import teams
+from django_openid_auth.forms import OpenIDLoginForm
+from django_openid_auth.models import UserOpenID
+from django_openid_auth.signals import openid_login_complete
+from django_openid_auth.store import DjangoOpenIDStore
+from django_openid_auth.exceptions import (
+    RequiredAttributeNotReturned,
+    DjangoOpenIDException,
+)
+
+
+next_url_re = re.compile('^/[-\w/]+$')
+
+def is_valid_next_url(next):
+    # When we allow this:
+    #   /openid/?next=/welcome/
+    # For security reasons we want to restrict the next= bit to being a local
+    # path, not a complete URL.
+    return bool(next_url_re.match(next))
+
+
+def sanitise_redirect_url(redirect_to):
+    """Sanitise the redirection URL."""
+    # Light security check -- make sure redirect_to isn't garbage.
+    is_valid = True
+    if not redirect_to or ' ' in redirect_to:
+        is_valid = False
+    elif '//' in redirect_to:
+        # Allow the redirect URL to be external if it's a permitted domain
+        allowed_domains = getattr(settings,
+            "ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS", [])
+        s, netloc, p, q, f = urlsplit(redirect_to)
+        # allow it if netloc is blank or if the domain is allowed
+        if netloc:
+            # a domain was specified. Is it an allowed domain?
+            if netloc.find(":") != -1:
+                netloc, _ = netloc.split(":", 1)
+            if netloc not in allowed_domains:
+                is_valid = False
+
+    # If the return_to URL is not valid, use the default.
+    if not is_valid:
+        redirect_to = settings.LOGIN_REDIRECT_URL
+
+    return redirect_to
+
+
+def make_consumer(request):
+    """Create an OpenID Consumer object for the given Django request."""
+    # Give the OpenID library its own space in the session object.
+    session = request.session.setdefault('OPENID', {})
+    store = DjangoOpenIDStore()
+    return Consumer(session, store)
+
+
+def render_openid_request(request, openid_request, return_to, trust_root=None):
+    """Render an OpenID authentication request."""
+    if trust_root is None:
+        trust_root = getattr(settings, 'OPENID_TRUST_ROOT',
+                             request.build_absolute_uri('/'))
+
+    if openid_request.shouldSendRedirect():
+        redirect_url = openid_request.redirectURL(
+            trust_root, return_to)
+        return HttpResponseRedirect(redirect_url)
+    else:
+        form_html = openid_request.htmlMarkup(
+            trust_root, return_to, form_tag_attrs={'id': 'openid_message'})
+        return HttpResponse(form_html, content_type='text/html;charset=UTF-8')
+
+
+def default_render_failure(request, message, status=403,
+                           template_name='openid/failure.html',
+                           exception=None):
+    """Render an error page to the user."""
+    data = render_to_string(
+        template_name, dict(message=message, exception=exception),
+        context_instance=RequestContext(request))
+    return HttpResponse(data, status=status)
+
+
+def parse_openid_response(request):
+    """Parse an OpenID response from a Django request."""
+    # Short cut if there is no request parameters.
+    #if len(request.REQUEST) == 0:
+    #    return None
+
+    current_url = request.build_absolute_uri()
+
+    consumer = make_consumer(request)
+    return consumer.complete(dict(request.REQUEST.items()), current_url)
+
+
+def login_begin(request, template_name='openid/login.html',
+                login_complete_view='openid-complete',
+                form_class=OpenIDLoginForm,
+                render_failure=default_render_failure,
+                redirect_field_name=REDIRECT_FIELD_NAME):
+    """Begin an OpenID login request, possibly asking for an identity URL."""
+    redirect_to = request.REQUEST.get(redirect_field_name, '')
+
+    # Get the OpenID URL to try.  First see if we've been configured
+    # to use a fixed server URL.
+    openid_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None)
+
+    if openid_url is None:
+        if request.POST:
+            login_form = form_class(data=request.POST)
+            if login_form.is_valid():
+                openid_url = login_form.cleaned_data['openid_identifier']
+        else:
+            login_form = form_class()
+
+        # Invalid or no form data:
+        if openid_url is None:
+            return render_to_response(template_name, {
+                    'form': login_form,
+                    redirect_field_name: redirect_to
+                    }, context_instance=RequestContext(request))
+
+    error = None
+    consumer = make_consumer(request)
+    try:
+        openid_request = consumer.begin(openid_url)
+    except DiscoveryFailure, exc:
+        return render_failure(
+            request, "OpenID discovery error: %s" % (str(exc),), status=500,
+            exception=exc)
+
+    # Request some user details.  If the provider advertises support
+    # for attribute exchange, use that.
+    if openid_request.endpoint.supportsType(ax.AXMessage.ns_uri):
+        fetch_request = ax.FetchRequest()
+        # We mark all the attributes as required, since Google ignores
+        # optional attributes.  We request both the full name and
+        # first/last components since some providers offer one but not
+        # the other.
+        for (attr, alias) in [
+            ('http://axschema.org/contact/email', 'email'),
+            ('http://axschema.org/namePerson', 'fullname'),
+            ('http://axschema.org/namePerson/first', 'firstname'),
+            ('http://axschema.org/namePerson/last', 'lastname'),
+            ('http://axschema.org/namePerson/friendly', 'nickname'),
+            # The myOpenID provider advertises AX support, but uses
+            # attribute names from an obsolete draft of the
+            # specification.  We request them for compatibility.
+            ('http://schema.openid.net/contact/email', 'old_email'),
+            ('http://schema.openid.net/namePerson', 'old_fullname'),
+            ('http://schema.openid.net/namePerson/friendly', 'old_nickname')]:
+            fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True))
+        openid_request.addExtension(fetch_request)
+    else:
+        sreg_required_fields = []
+        sreg_required_fields.extend(
+            getattr(settings, 'OPENID_SREG_REQUIRED_FIELDS', []))
+        sreg_optional_fields = ['email', 'fullname', 'nickname']
+        sreg_optional_fields.extend(
+            getattr(settings, 'OPENID_SREG_EXTRA_FIELDS', []))
+        sreg_optional_fields = [
+            field for field in sreg_optional_fields if (
+                not field in sreg_required_fields)]
+        openid_request.addExtension(
+            sreg.SRegRequest(optional=sreg_optional_fields,
+                required=sreg_required_fields))
+            
+    if getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False):
+        preferred_auth = [
+            pape.AUTH_MULTI_FACTOR_PHYSICAL,
+        ]
+        pape_request = pape.Request(preferred_auth_policies=preferred_auth)
+        openid_request.addExtension(pape_request)
+
+    # Request team info
+    teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False)
+    teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', [])
+    launchpad_teams = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+    if teams_mapping_auto:
+        #ignore launchpad teams. use all django-groups
+        launchpad_teams = dict()
+        all_groups = Group.objects.exclude(name__in=teams_mapping_auto_blacklist)
+        for group in all_groups:
+            launchpad_teams[group.name] = group.name
+
+    if launchpad_teams:
+        openid_request.addExtension(teams.TeamsRequest(launchpad_teams.keys()))
+
+    # Construct the request completion URL, including the page we
+    # should redirect to.
+    return_to = request.build_absolute_uri(reverse(login_complete_view))
+    if redirect_to:
+        if '?' in return_to:
+            return_to += '&'
+        else:
+            return_to += '?'
+        # Django gives us Unicode, which is great.  We must encode URI.
+        # urllib enforces str. We can't trust anything about the default
+        # encoding inside  str(foo) , so we must explicitly make foo a str.
+        return_to += urllib.urlencode(
+            {redirect_field_name: redirect_to.encode("UTF-8")})
+
+    return render_openid_request(request, openid_request, return_to)
+
+
+@csrf_exempt
+def login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME,
+                   render_failure=None):
+    redirect_to = request.REQUEST.get(redirect_field_name, '')
+    render_failure = render_failure or \
+                     getattr(settings, 'OPENID_RENDER_FAILURE', None) or \
+                     default_render_failure
+
+    openid_response = parse_openid_response(request)
+    if not openid_response:
+        return render_failure(
+            request, 'This is an OpenID relying party endpoint.')
+
+    if openid_response.status == SUCCESS:
+        try:
+            user = authenticate(openid_response=openid_response)
+        except DjangoOpenIDException, e:
+            return render_failure(request, e.message, exception=e)
+            
+        if user is not None:
+            if user.is_active:
+                auth_login(request, user)
+                response = HttpResponseRedirect(sanitise_redirect_url(redirect_to))
+
+                # Notify any listeners that we successfully logged in.
+                openid_login_complete.send(sender=UserOpenID, request=request,
+                    openid_response=openid_response)
+
+                return response
+            else:
+                return render_failure(request, 'Disabled account')
+        else:
+            return render_failure(request, 'Unknown user')
+    elif openid_response.status == FAILURE:
+        return render_failure(
+            request, 'OpenID authentication failed: %s' %
+            openid_response.message)
+    elif openid_response.status == CANCEL:
+        return render_failure(request, 'Authentication cancelled')
+    else:
+        assert False, (
+            "Unknown OpenID response type: %r" % openid_response.status)
+
+
+def logo(request):
+    return HttpResponse(
+        OPENID_LOGO_BASE_64.decode('base64'), mimetype='image/gif'
+    )
+
+# Logo from http://openid.net/login-bg.gif
+# Embedded here for convenience; you should serve this as a static file
+OPENID_LOGO_BASE_64 = """
+R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d
+3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA
+AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg
+EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD
+Fzk0lpcjIQA7
+"""

+ 28 - 0
desktop/core/ext-py/django-openid-auth-0.5/example_consumer/__init__.py

@@ -0,0 +1,28 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.

+ 11 - 0
desktop/core/ext-py/django-openid-auth-0.5/example_consumer/manage.py

@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)

+ 145 - 0
desktop/core/ext-py/django-openid-auth-0.5/example_consumer/settings.py

@@ -0,0 +1,145 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+# Django settings for example project.
+import django
+django_version = django.get_version()
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+if django_version >= "1.2": 
+    csrf_middleware = 'django.middleware.csrf.CsrfViewMiddleware'
+    DATABASES = {
+                     'default': {
+                         'ENGINE': 'django.db.backends.sqlite3',
+                         'NAME': 'sqlite.db'
+                     }
+                 }
+    TEMPLATE_LOADERS = (
+        'django.template.loaders.filesystem.Loader',
+        'django.template.loaders.app_directories.Loader',
+    )
+else:
+    csrf_middleware = 'django.contrib.csrf.middleware.CsrfViewMiddleware'
+    TEMPLATE_LOADERS = (
+        'django.template.loaders.filesystem.load_template_source',
+        'django.template.loaders.app_directories.load_template_source',
+     )
+    DATABASE_ENGINE = 'sqlite3'           # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
+    DATABASE_NAME = 'sqlite.db'             # Or path to database file if using sqlite3.
+    DATABASE_USER = ''             # Not used with sqlite3.
+    DATABASE_PASSWORD = ''         # Not used with sqlite3.
+    DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
+    DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
+# although not all variations may be possible on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+# http://blogs.law.harvard.edu/tech/stories/storyReader$15
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT.
+# Example: "http://media.lawrence.com"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = '34958734985734985734985798437'
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    csrf_middleware,
+)
+
+ROOT_URLCONF = 'example_consumer.urls'
+
+TEMPLATE_DIRS = (
+    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.admin',
+    'django_openid_auth',
+)
+
+AUTHENTICATION_BACKENDS = (
+    'django_openid_auth.auth.OpenIDBackend',
+    'django.contrib.auth.backends.ModelBackend',
+)
+
+# Should users be created when new OpenIDs are used to log in?
+OPENID_CREATE_USERS = True
+
+# When logging in again, should we overwrite user details based on
+# data received via Simple Registration?
+OPENID_UPDATE_DETAILS_FROM_SREG = True
+
+# If set, always use this as the identity URL rather than asking the
+# user.  This only makes sense if it is a server URL.
+OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
+
+# Tell django.contrib.auth to use the OpenID signin URLs.
+LOGIN_URL = '/openid/login/'
+LOGIN_REDIRECT_URL = '/'
+
+# Should django_auth_openid be used to sign into the admin interface?
+OPENID_USE_AS_ADMIN_LOGIN = False

+ 45 - 0
desktop/core/ext-py/django-openid-auth-0.5/example_consumer/urls.py

@@ -0,0 +1,45 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.conf.urls.defaults import *
+from django.contrib import admin
+
+import views
+
+
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    (r'^$', views.index),
+    (r'^openid/', include('django_openid_auth.urls')),
+    (r'^logout/$', 'django.contrib.auth.views.logout'),
+    (r'^private/$', views.require_authentication),
+
+    (r'^admin/', include(admin.site.urls)),
+)

+ 57 - 0
desktop/core/ext-py/django-openid-auth-0.5/example_consumer/views.py

@@ -0,0 +1,57 @@
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse
+from django.utils.html import escape
+
+
+def index(request):
+    s = ['<p>']
+    if request.user.is_authenticated():
+        s.append('You are signed in as <strong>%s</strong> (%s)' % (
+                escape(request.user.username),
+                escape(request.user.get_full_name())))
+        s.append(' | <a href="/logout">Sign out</a>')
+    else:
+        s.append('<a href="/openid/login">Sign in with OpenID</a>')
+
+    s.append('</p>')
+
+    s.append('<p><a href="/private">This requires authentication</a></p>')
+    return HttpResponse('\n'.join(s))
+
+
+def next_works(request):
+    return HttpResponse('?next= bit works. <a href="/">Home</a>')
+
+
+@login_required
+def require_authentication(request):
+    return HttpResponse('This page requires authentication')

+ 80 - 0
desktop/core/ext-py/django-openid-auth-0.5/setup.py

@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# django-openid-auth -  OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009-2013 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+"""OpenID integration for django.contrib.auth
+
+A library that can be used to add OpenID support to Django applications.
+The library integrates with Django's built in authentication system, so
+most applications require minimal changes to support OpenID llogin. The
+library also includes the following features:
+  * Basic user details are transferred from the OpenID server via the
+    Simple Registration extension or Attribute Exchange extension.
+  * can be configured to use a fixed OpenID server URL, for use in SSO.
+  * supports the launchpad.net teams extension to get team membership
+    info.
+"""
+
+from distutils.core import setup
+
+
+description, long_description = __doc__.split('\n\n', 1)
+VERSION = '0.5'
+
+setup(
+    name='django-openid-auth',
+    version=VERSION,
+    author='Canonical Ltd',
+    description=description,
+    long_description=long_description,
+    license='BSD',
+    platforms=['any'],
+    url='https://launchpad.net/django-openid-auth',
+    download_url=('http://launchpad.net/django-openid-auth/trunk/%s/+download'
+                  '/django-openid-auth-%s.tar.gz' % (VERSION, VERSION)),
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules'
+        ],
+    packages=[
+        'django_openid_auth',
+        'django_openid_auth.management',
+        'django_openid_auth.management.commands',
+        'django_openid_auth.tests',
+        ],
+    package_data={
+        'django_openid_auth': ['templates/openid/*.html'],
+        },
+    provides=['django_openid_auth'],
+    requires=['django (>=1.1.2)', 'openid (>=2.2.0)'],
+    )

+ 202 - 0
desktop/core/ext-py/python-openid-2.2.5/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 7 - 0
desktop/core/ext-py/python-openid-2.2.5/MANIFEST.in

@@ -0,0 +1,7 @@
+include LICENSE NOTICE CHANGELOG MANIFEST.in NEWS background-associations.txt
+graft admin
+graft contrib
+recursive-include examples README discover *.py *.html *.xml
+recursive-include openid/test *.txt dhpriv n2b64 *.py
+recursive-include openid/test/data *
+recursive-include doc *.css *.html

+ 225 - 0
desktop/core/ext-py/python-openid-2.2.5/NEWS

@@ -0,0 +1,225 @@
+What's New in Python OpenID 2.1.0
+=================================
+
+This implementation of OpenID has been upgraded to support version 2.0
+of the OpenID Authentication specification.
+
+New in this version is:
+
+ * Verification of relying party return_to addresses, to screen out RPs
+   hiding behind open redirect relays.  Server code can invoke this with
+   the returnToVerified method on CheckIDRequest.
+
+ * Helper module for the Provider Authentication Policy Extension (PAPE) in
+   openid.extensions.pape.
+
+ * Helper module for Attribute Exchange in openid.extensions.ax.
+
+Bugfixes:
+
+ * Allow the use of lxml as an ElementTree implemenation.
+
+ * Provide compatability with a wider range of versions for SQL stores.
+
+
+Upgrading from 2.0.1
+--------------------
+
+The third argument to Consumer.complete() is required.
+
+The sreg module should be imported from openid.extensions.sreg instead of
+openid.sreg.
+
+The ax module should likewise be imported from openid.extensions.ax
+instead of openid.ax
+
+The openid.extensions.ax.FetchRequest.fromOpenIDRequest method now
+takes a CheckIDRequest object instead of a Message object
+
+The OpenID response (the result of Consumer.complete()) now has a
+getDisplayIdentifier() method which should be called instead of
+accessing response.identity_url.  The value of getDisplayIdentifier()
+will be the XRI i-name if XRI is used.  The value of
+response.identity_url SHOULD, however, be used as the application's
+database key for storing account information.
+
+What's New in Python OpenID 2.0
+===============================
+
+The big news here is compatibility with svn revision 313 of the OpenID 2.0 
+draft specification.
+
+Highlights include:
+
+ * Simple Registration support in a new module openid.sreg.  (Those
+   previously using SuccessResponse.extensionResponse are advised to
+   look here.)
+ * OpenID provider-driven identifier selection.
+ * "Negotiators" allow you to define which association types to use.
+ * Examples for Django.
+
+Dependencies
+------------
+
+Python 2.5 is now supported.  Support for Python 2.2 discontinued.
+Seperate installation of yadis and urljr packages is no longer
+required; they have been included in this package.  
+
+
+Upgrading from 1.1 or 1.2
+-------------------------
+
+One of the additions to the OpenID protocol was a specified nonce
+format for one-way nonces.  As a result, the nonce table in the store
+has changed.  You'll need to run contrib/upgrade-store-1.1-to-2.0 to
+upgrade your store, or you'll encounter errors about the wrong number
+of columns in the oid_nonces table.
+
+If you've written your own custom store or code that interacts directly with it,
+you'll need to review the change notes in openid.store.interface.
+
+Consumers should now pass an additional parameter to Consumer.complete() 
+to defend against return_to tampering.
+
+
+What's New in Python OpenID 1.1.2
+=================================
+
+i-name Support
+--------------
+
+This version of the library allows the use of XRI as OpenID identifiers,
+allowing users to log in with their i-names.  For full XRI compatibility,
+relying parties integrating this library should take note of the user's
+CanonicalID, as described in the "Identifying the End User" section of the
+OpenID 2.0 specification.
+
+Bug Fixes
+---------
+
+A variety of bug fixes were included in this release, mostly relating to
+international issues such as dealing with other character sets, Unicode,
+incorrectly flagging certain Norwegian trust roots as suspect, and operation
+of the filesystem-backed store on exotic platforms.
+
+Dependencies
+------------
+
+ * urljr 1.0.1
+ * yadis 1.1.0
+
+
+What's New in Python OpenID 1.1.0
+=================================
+
+Version 1.1 of the Python OpenID library implements recent changes to
+the OpenID specification as well as making API changes that should
+make integration with applications easier.
+
+Yadis Support
+-------------
+
+One of the major changes to OpenID since the last release has been the
+approval of Yadis discovery as the preferred way to specify the OpenID
+metadata for an identity URL instead of using <link> tags in
+HTML. This library does Yadis discovery, and if that fails, it falls
+back to old-style discovery.
+
+Some advantages of Yadis support are:
+
+ * Support for fallback if your primary OpenID provider is not available
+
+ * Support for load-balancing between OpenID servers
+
+ * Easy interoperability for different identity services
+
+For more information about Yadis, see http://yadis.org/
+
+Extension Support
+-----------------
+
+OpenID also has formalized support for extensions. Extensions are a
+mechanism for transferring information from the consumer to the server
+and from the server to the consumer in the process of performing
+OpenID authentication. Extensions are implemented as additional
+namespaced query arguments that go along with standard OpenID requests
+and responses. This library provides a simple API for adding extension
+arguments to requests and extracting extension responses from replies.
+
+Dependencies
+------------
+
+These dependencies should be available from wherever you acquired the
+OpenID library.
+
+ * urljr - The fetcher abstraction from the previous OpenID library
+   has been extended and is also used for the Yadis library. Because
+   the Yadis library is useful without the OpenID library, the HTTP
+   fetching code has been rolled into its own package. Additionally,
+   the library now has the concept of a default fetcher to make APIs
+   simpler.
+
+ * yadis - The Yadis library provides a general discovery layer that
+   has been adopted by OpenID as well as other identity-related
+   protocols. Most OpenID identity URLs will work without the Yadis
+   library, but as time goes on, this library will be more and more
+   important.
+
+Consumer API
+------------
+
+The consumer API has been changed for more natural use as well as to
+support extension arguments.
+
+ * OpenIDConsumer(store, [fetcher], [immediate]) is now
+   Consumer(session, store)
+
+  - The session object is a dictionary-like object that should be
+    tied to the requesting HTTP agent, for example, using a session
+    ID cookie. It is used for Yadis fallback and holding the state
+    of the OpenID transaction between the redirect to the server
+    and the response. The values that are placed in the session are
+    namespaced, so there should not be a conflict with other uses
+    of the same session. The session namespace is an attribute of
+    the Consumer object.
+
+  - Because the consumer object now does session management, it is
+    necessary to construct a new consumer object for every
+    request. Creating consumer objects is light-weight.
+
+ * OpenIDConsumer.beginAuth(user_url) is now Consumer.begin(user_url)
+   and either returns an AuthRequest object or raises an
+   exception. There is no more tuple unpacking or status codes.
+
+ * OpenIDConsumer.constructRedirect(authreq, return_to, trust_root) is
+   now AuthRequest.redirectURL(trust_root, return_to, [immediate]).
+
+ * OpenIDConsumer.completeAuth(token, query) is now
+   Consumer.complete(query). It no longer returns a tuple. Instead it
+   returns an object that has a status code and additional information
+   about the response. See the API documentation for more information.
+
+Server API
+----------
+
+The server API has been changed for greater extensibility.  Instead
+of taking an "is_authorized" callback, processing happens in several
+stages, allowing you to insert extension data into the response
+before it is signed and returned.  See the documentation for the
+openid.server.server module.
+
+Fetcher API
+-----------
+
+ * fetcher was openid.consumer.fetchers.OpenIDHTTPFetcher, is now
+   urljr.fetchers.HTTPFetcher.  get() and post() have been replaced by
+   fetch(), see urljr.fetchers for details.
+
+Upgrading from 1.0
+------------------
+
+The server changed the way it indexes associations in the store, so if
+you're upgrading a server installation, we recommend you clear the old
+records from your store when you do so.  As a consequence, consumers
+will re-establish associations with your server a little sooner than
+they would have otherwise.

+ 4 - 0
desktop/core/ext-py/python-openid-2.2.5/NOTICE

@@ -0,0 +1,4 @@
+Python OpenID may be obtained from
+http://github.com/openid/python-openid
+and we'd like to hear about how you're using this software.
+Write to us at openid@janrain.com.

+ 25 - 0
desktop/core/ext-py/python-openid-2.2.5/PKG-INFO

@@ -0,0 +1,25 @@
+Metadata-Version: 1.0
+Name: python-openid
+Version: 2.2.5
+Summary: OpenID support for servers and consumers.
+Home-page: http://github.com/openid/python-openid
+Author: JanRain
+Author-email: openid@janrain.com
+License: UNKNOWN
+Download-URL: http://github.com/openid/python-openid/tarball/2.2.5
+Description: This is a set of Python packages to support use of
+        the OpenID decentralized identity system in your application.  Want to enable
+        single sign-on for your web site?  Use the openid.consumer package.  Want to
+        run your own OpenID server? Check out openid.server.  Includes example code
+        and support for a variety of storage back-ends.
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: System :: Systems Administration :: Authentication/Directory

+ 68 - 0
desktop/core/ext-py/python-openid-2.2.5/README

@@ -0,0 +1,68 @@
+This is the Python OpenID library.
+
+REQUIREMENTS
+============
+
+ - Python 2.3, 2.4, or 2.5.
+
+ - ElementTree.  This is included in the Python 2.5 standard library,
+   but users of earlier versions of Python may need to install it
+   seperately.
+
+ - pycrypto, if on Python 2.3 and without /dev/urandom, or on Python
+   2.3 or 2.4 and you want SHA256.
+
+
+INSTALLATION
+============
+
+To install the base library, just run the following command:
+
+python setup.py install
+
+To run setup.py you need the distutils module from the Python standard
+library; some distributions package this seperately in a "python-dev"
+package.
+
+
+GETTING STARTED
+===============
+
+The examples directory includes an example server and consumer
+implementation.  See the README file in that directory for more
+information on running the examples.
+
+Library documentation is available in html form in the doc directory.
+
+
+LOGGING
+=======
+
+This library offers a logging hook that will record unexpected
+conditions that occur in library code. If a condition is recoverable,
+the library will recover and issue a log message. If it is not
+recoverable, the library will raise an exception. See the
+documentation for the openid.oidutil module for more on the logging
+hook.
+
+
+DOCUMENTATION
+=============
+
+The documentation in this library is in Epydoc format, which is
+detailed at:
+
+  http://epydoc.sourceforge.net/
+
+
+CONTACT
+=======
+
+Send bug reports, suggestions, comments, and questions to
+http://openid.net/developers/dev-mailing-lists/.
+
+If you have a bugfix or feature you'd like to contribute, don't
+hesitate to send it to us.  For more detailed information on how to
+contribute, see
+
+  http://openidenabled.com/contribute/

+ 67 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/builddiscover.py

@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+import os.path
+import urlparse
+
+from openid.test import discoverdata
+
+manifest_header = """\
+# This file contains test cases for doing YADIS identity URL and
+# service discovery. For each case, there are three URLs. The first
+# URL is the user input. The second is the identity URL and the third
+# is the URL from which the XRDS document should be read.
+#
+# The file format is as follows:
+# User URL <tab> Identity URL <tab> XRDS URL <newline>
+#
+# blank lines and lines starting with # should be ignored.
+#
+# To use this test:
+#
+# 1. Run your discovery routine on the User URL.
+#
+# 2. Compare the identity URL returned by the discovery routine to the
+#    identity URL on that line of the file. It must be an EXACT match.
+#
+# 3. Do a regular HTTP GET on the XRDS URL. Compare the content that
+#    was returned by your discovery routine with the content returned
+#    from that URL. It should also be an exact match.
+
+"""
+
+def buildDiscover(base_url, out_dir):
+    """Convert all files in a directory to apache mod_asis files in
+    another directory."""
+    test_data = discoverdata.readTests(discoverdata.default_test_file)
+
+    def writeTestFile(test_name):
+        template = test_data[test_name]
+
+        data = discoverdata.fillTemplate(
+            test_name, template, base_url, discoverdata.example_xrds)
+
+        out_file_name = os.path.join(out_dir, test_name)
+        out_file = file(out_file_name, 'w')
+        out_file.write(data)
+
+    manifest = [manifest_header]
+    for success, input_name, id_name, result_name in discoverdata.testlist:
+        if not success:
+            continue
+        writeTestFile(input_name)
+
+        input_url = urlparse.urljoin(base_url, input_name)
+        id_url = urlparse.urljoin(base_url, id_name)
+        result_url = urlparse.urljoin(base_url, result_name)
+
+        manifest.append('\t'.join((input_url, id_url, result_url)))
+        manifest.append('\n')
+
+    manifest_file_name = os.path.join(out_dir, 'manifest.txt')
+    manifest_file = file(manifest_file_name, 'w')
+    for chunk in manifest:
+        manifest_file.write(chunk)
+    manifest_file.close()
+
+if __name__ == '__main__':
+    import sys
+    buildDiscover(*sys.argv[1:])

+ 10 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/fixperms

@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+cat - <<EOF | xargs chmod +x
+admin/builddiscover.py
+admin/fixperms
+admin/makechangelog
+admin/pythonsource
+admin/runtests
+admin/setversion
+admin/tagrelease
+EOF

+ 47 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/gettlds.py

@@ -0,0 +1,47 @@
+"""
+Fetch the current TLD list from the IANA Web site, parse it, and print
+an expression suitable for direct insertion into each library's trust
+root validation module
+
+Usage:
+  python gettlds.py (php|python|ruby)
+
+Then cut-n-paste.
+"""
+
+import urllib2
+
+import sys
+
+langs = {
+    'php': (r"'/\.(",
+            "'", "|", "|' .",
+            r")\.?$/'"),
+    'python': ("['",
+               "'", "', '", "',",
+               "']"),
+    'ruby': ("%w'",
+             "", " ", "",
+             "'"),
+    }
+
+lang = sys.argv[1]
+prefix, line_prefix, separator, line_suffix, suffix = langs[lang]
+
+f = urllib2.urlopen('http://data.iana.org/TLD/tlds-alpha-by-domain.txt')
+tlds = []
+output_line = ""
+for input_line in f:
+    if input_line.startswith('#'):
+        continue
+
+    tld = input_line.strip().lower()
+    new_output_line = output_line + prefix + tld
+    if len(new_output_line) > 60:
+        print output_line + line_suffix
+        output_line = line_prefix + tld
+    else:
+        output_line = new_output_line
+    prefix = separator
+
+print output_line + suffix

+ 2 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/makechangelog

@@ -0,0 +1,2 @@
+#!/bin/bash
+darcs changes --from-tag '^release-' --summary > CHANGELOG

+ 7 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/makedoc

@@ -0,0 +1,7 @@
+#!/bin/bash
+
+rm -rf doc/*
+epydoc --html --output doc/ --name "Python-OpenID" --top openid \
+    --url "http://openidenabled.com/python-openid/" --inheritance listed \
+    --no-private -v \
+    $( find openid -name '*.py' -and -not -path 'openid/test*' )

+ 2 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/pythonsource

@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+find ./openid/ \( \( -name _darcs -o -name .svn -o -name _trial_temp -o -name test -o -name admin -o -name examples \) -type d -prune -a -false \) -o \( ! -iname \*~ -a -type f \) -a \( ! -iname \*.pyc -a -type f \) -a -name \*.py

+ 204 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/runtests

@@ -0,0 +1,204 @@
+#!/usr/bin/env python
+import os.path, sys, warnings
+
+test_modules = [
+    'cryptutil',
+    'oidutil',
+    'dh',
+    ]
+
+def fixpath():
+    try:
+        d = os.path.dirname(__file__)
+    except NameError:
+        d = os.path.dirname(sys.argv[0])
+    parent = os.path.normpath(os.path.join(d, '..'))
+    if parent not in sys.path:
+        print "putting %s in sys.path" % (parent,)
+        sys.path.insert(0, parent)
+
+def otherTests():
+    failed = []
+    for module_name in test_modules:
+        print 'Testing %s...' % (module_name,) ,
+        sys.stdout.flush()
+        module_name = 'openid.test.' + module_name
+        try:
+            test_mod = __import__(module_name, {}, {}, [None])
+        except ImportError:
+            print 'Failed to import test %r' % (module_name,)
+            failed.append(module_name)
+        else:
+            try:
+                test_mod.test()
+            except (SystemExit, KeyboardInterrupt):
+                raise
+            except:
+                sys.excepthook(*sys.exc_info())
+                failed.append(module_name)
+            else:
+                print 'Succeeded.'
+
+
+    return failed
+
+def pyunitTests():
+    import unittest
+    pyunit_module_names = [
+        'server',
+        'consumer',
+        'message',
+        'symbol',
+        'etxrd',
+        'xri',
+        'xrires',
+        'association_response',
+        'auth_request',
+        'negotiation',
+        'verifydisco',
+        'sreg',
+        'ax',
+        'pape',
+        'pape_draft2',
+        'pape_draft5',
+        'rpverify',
+        'extension',
+        ]
+
+    pyunit_modules = [
+        __import__('openid.test.test_%s' % (name,), {}, {}, ['unused'])
+        for name in pyunit_module_names
+        ]
+
+    try:
+        from openid.test import test_examples
+    except ImportError, e:
+        if 'twill' in str(e):
+            warnings.warn("Could not import twill; skipping test_examples.")
+        else:
+            raise
+    else:
+        pyunit_modules.append(test_examples)
+
+    # Some modules have data-driven tests, and they use custom methods
+    # to build the test suite:
+    custom_module_names = [
+        'kvform',
+        'linkparse',
+        'oidutil',
+        'storetest',
+        'test_accept',
+        'test_association',
+        'test_discover',
+        'test_fetchers',
+        'test_htmldiscover',
+        'test_nonce',
+        'test_openidyadis',
+        'test_parsehtml',
+        'test_urinorm',
+        'test_yadis_discover',
+        'trustroot',
+        ]
+
+    loader = unittest.TestLoader()
+    s = unittest.TestSuite()
+
+    for m in pyunit_modules:
+        s.addTest(loader.loadTestsFromModule(m))
+
+    for name in custom_module_names:
+        m = __import__('openid.test.%s' % (name,), {}, {}, ['unused'])
+        try:
+            s.addTest(m.pyUnitTests())
+        except AttributeError, ex:
+            # because the AttributeError doesn't actually say which
+            # object it was.
+            print "Error loading tests from %s:" % (name,)
+            raise
+
+    runner = unittest.TextTestRunner() # verbosity=2)
+
+    return runner.run(s)
+
+
+
+def splitDir(d, count):
+    # in python2.4 and above, it's easier to spell this as
+    # d.rsplit(os.sep, count)
+    for i in xrange(count):
+        d = os.path.dirname(d)
+    return d
+
+
+
+def _import_djopenid():
+    """Import djopenid from examples/
+
+    It's not in sys.path, and I don't really want to put it in sys.path.
+    """
+    import types
+    thisfile = os.path.abspath(sys.modules[__name__].__file__)
+    topDir = splitDir(thisfile, 2)
+    djdir = os.path.join(topDir, 'examples', 'djopenid')
+
+    djinit = os.path.join(djdir, '__init__.py')
+
+    djopenid = types.ModuleType('djopenid')
+    execfile(djinit, djopenid.__dict__)
+    djopenid.__file__ = djinit
+
+    # __path__ is the magic that makes child modules of the djopenid package
+    # importable.  New feature in python 2.3, see PEP 302.
+    djopenid.__path__ = [djdir]
+    sys.modules['djopenid'] = djopenid
+
+
+
+def django_tests():
+    """Runs tests from examples/djopenid.
+
+    @returns: number of failed tests.
+    """
+    import os
+    # Django uses this to find out where its settings are.
+    os.environ['DJANGO_SETTINGS_MODULE'] = 'djopenid.settings'
+
+    _import_djopenid()
+
+    try:
+        import django.test.simple
+    except ImportError, e:
+        warnings.warn("django.test.simple not found; "
+                      "django examples not tested.")
+        return 0
+    import djopenid.server.models, djopenid.consumer.models
+    print "Testing Django examples:"
+
+    # These tests do get put in to a pyunit test suite, so we could run them
+    # with the other pyunit tests, but django also establishes a test database
+    # for them, so we let it do that thing instead.
+    return django.test.simple.run_tests([djopenid.server.models,
+                                         djopenid.consumer.models])
+
+try:
+    bool
+except NameError:
+    def bool(x):
+        return not not x
+
+def main():
+    fixpath()
+    other_failed = otherTests()
+    pyunit_result = pyunitTests()
+    django_failures = django_tests()
+
+    if other_failed:
+        print 'Failures:', ', '.join(other_failed)
+
+    failed = (bool(other_failed) or
+              bool(not pyunit_result.wasSuccessful()) or
+              (django_failures > 0))
+    return failed
+
+if __name__ == '__main__':
+    sys.exit(main() and 1 or 0)

+ 7 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/setversion

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+cat <<EOF | \
+    xargs sed -i 's/\[library version:[^]]*\]/[library version:'"$1"']/'
+setup.py
+openid/__init__.py
+EOF

+ 13 - 0
desktop/core/ext-py/python-openid-2.2.5/admin/tagrelease

@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+VERSION="$1"
+VERSION_PATTERN='^[0-9]\+\.[0-9]\+\.[0-9]\+\(-[a-z0-9-]\+\)\?$'
+
+echo "$VERSION" | grep -e "$VERSION_PATTERN" 2>&1 >/dev/null || {
+    echo "$VERSION"
+    echo "Malformed version number. Expected X.X.X or X.X.X-name." 1>&2
+    exit 1
+}
+
+cd $(dirname $(dirname $(readlink --canonicalize "$0")))
+./admin/setversion "$VERSION"
+darcs record -m 'Set version number to '"$VERSION" && darcs tag "release-$VERSION"

+ 96 - 0
desktop/core/ext-py/python-openid-2.2.5/background-associations.txt

@@ -0,0 +1,96 @@
+Background association requests
+###############################
+
+This document describes how to make signing in with OpenID faster for
+users of your application by never making the users wait for an
+association to be made, but using associations when they're
+available. Most OpenID libraries and applications attempt to make
+associations during the discovery phase of the OpenID authentication
+request. Because association requests may have to do Diffie-Hellman
+key exchange, which is time consuming. Even if Diffie-Hellman key
+exchange is not used, the user still needs to wait for the association
+request.
+
+Setting up your application to make associations in the background
+==================================================================
+
+When making associations background, there are two components that
+need access to the OpenID association store: the consumer application
+and the background association fetcher. The consumer needs to be set
+up to record the server URL for any request for which an association
+does not exist or is expired instead of making a new association. The
+background fetcher looks at the server URL queue and makes
+associations for any server URLs that need them. After the
+associations are made, the consumer will use them until they expire
+again. While associations are expired or missing, the consumer will
+use stateless mode to complete authentications with the servers that
+need associations.
+
+The OpenID server endpoint URL queue
+-----------------------------------------------------------------
+
+You will have to set up a conduit between the consumer and the
+background association fetcher so that the background association
+fetcher knows what servers need associations. The background
+association fetcher will not fetch associations for servers that
+already have them, so the queue does not have to be very smart. It
+could be as simple as a file to which the server URLs are
+appended. Either way, the queue needs to be write-able by the consumer
+and readable by the background fetcher.
+
+Configuring the consumer
+-----------------------------------------------------------------
+
+Create a subclass of ``GenericConsumer`` that overrides
+``_negotiateAssociation`` so that it just records the server URL that
+needs an association::
+
+  from openid.consumer.consumer import GenericConsumer, Consumer
+
+  class LazyAssociationConsumer(GenericConsumer):
+      needs_assoc_file = None
+
+      def _negotiateAssociation(self, endpoint):
+          # Do whatever you need to do here to send the server_url to
+          # the queue. This example just appends it to a file.
+          self.needs_assoc_file.write(endpoint.server_url + '\n')
+          self.needs_assoc_file.flush()
+
+You could also store the whole endpoint object. When you instantiate
+the consumer, pass this generic consumer class to the controlling
+consumer::
+
+  return Consumer(session, store, consumer_class=LazyAssociationConsumer)
+
+The background association fetcher
+-----------------------------------------------------------------
+
+The background association fetcher is just a script that should be
+added to ``cron`` or triggered periodically. If you are ambitious, you
+could make the background fetcher listen for inserts into the queue.
+
+The background fetcher needs to do something similar to the following::
+
+  def refresh(consumer, endpoint):
+      if consumer.store.getAssociation(endpoint.server_url):
+          logging.info("We don't need to associate with %r", endpoint.server_url)
+          return
+
+      logging.info("Associating with %r", endpoint.server_url)
+      now = time.time()
+      assoc = consumer._negotiateAssociation(endpoint)
+      if assoc:
+          elapsed = time.time() - now
+          logging.info('(%0.2f seconds) Associated with %r', elapsed,
+                       endpoint.server_url)
+          consumer.store.storeAssociation(endpoint.server_url, assoc)
+      else:
+          logging.error('Failed to make an association with %r',
+                        endpoint.server_url)
+
+The code in this example logs the amount of time that the association
+request took. This is time that the user would have been waiting. The
+``consumer`` in this example is a standard consumer, not the
+``LazyAssociationConsumer`` that was defined in the section
+above. This is important, because the lazy consumer above will not
+actually make any associations.

+ 47 - 0
desktop/core/ext-py/python-openid-2.2.5/contrib/associate

@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+"""Make an OpenID Assocition request against an endpoint
+and print the results."""
+
+import sys
+
+from openid.store.memstore import MemoryStore
+from openid.consumer import consumer
+from openid.consumer.discover import OpenIDServiceEndpoint
+
+from datetime import datetime
+
+def verboseAssociation(assoc):
+    """A more verbose representation of an Association.
+    """
+    d = assoc.__dict__
+    issued_date = datetime.fromtimestamp(assoc.issued)
+    d['issued_iso'] = issued_date.isoformat()
+    fmt = """  Type: %(assoc_type)s
+  Handle: %(handle)s
+  Issued: %(issued)s [%(issued_iso)s]
+  Lifetime: %(lifetime)s
+  Secret: %(secret)r
+"""
+    return fmt % d
+
+def main():
+    if not sys.argv[1:]:
+        print "Usage: %s ENDPOINT_URL..." % (sys.argv[0],)
+    for endpoint_url in sys.argv[1:]:
+        print "Associating with", endpoint_url
+
+        # This makes it clear why j3h made AssociationManager when we
+        # did the ruby port.  We can't invoke requestAssociation
+        # without these other trappings.
+        store = MemoryStore()
+        endpoint = OpenIDServiceEndpoint()
+        endpoint.server_url = endpoint_url
+        c = consumer.GenericConsumer(store)
+        auth_req = c.begin(endpoint)
+        if auth_req.assoc:
+            print verboseAssociation(auth_req.assoc)
+        else:
+            print "  ...no association."
+
+if __name__ == '__main__':
+    main()

+ 151 - 0
desktop/core/ext-py/python-openid-2.2.5/contrib/openid-parse

@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+"""Grab URLs from the clipboard, interpret the queries as OpenID, and print.
+
+In addition to URLs, I also scan for queries as they appear in httpd log files,
+with a pattern like 'GET /foo?bar=baz HTTP'.
+
+Requires the 'xsel' program to get the contents of the clipboard.
+"""
+
+from pprint import pformat
+from urlparse import urlsplit, urlunsplit
+import cgi, re, subprocess, sys
+
+from openid import message
+
+OPENID_SORT_ORDER = ['mode', 'identity', 'claimed_id']
+
+class NoQuery(Exception):
+    def __init__(self, url):
+        self.url = url
+
+    def __str__(self):
+        return "No query in url %s" % (self.url,)
+
+
+def getClipboard():
+    xsel = subprocess.Popen(["xsel", "-o", "-b"], stdout=subprocess.PIPE)
+    output = xsel.communicate()[0]
+    return output
+
+
+def main():
+    source = getClipboard()
+    urls = find_urls(source)
+
+    errors = []
+    output = []
+    queries = []
+
+    queries.extend(queriesFromPostdata(source))
+
+    for url in urls:
+        try:
+            queries.append(queryFromURL(url))
+        except NoQuery, err:
+            errors.append(err)
+
+    queries.extend(queriesFromLogs(source))
+
+    for where, query in queries:
+        output.append('at %s:\n%s' % (where, openidFromQuery(query)))
+
+    if output:
+        print '\n\n'.join(output)
+    elif errors:
+        for err in errors:
+            print err
+
+
+def queryFromURL(url):
+    split_url = urlsplit(url)
+    query = cgi.parse_qs(split_url[3])
+
+    if not query:
+        raise NoQuery(url)
+
+    url_without_query = urlunsplit(split_url[:3] + (None, None))
+
+    return (url_without_query, query)
+
+
+def openidFromQuery(query):
+    try:
+        msg = message.Message.fromPostArgs(unlistify(query))
+        s = formatOpenIDMessage(msg)
+    except Exception, err:
+        # XXX - side effect.
+        sys.stderr.write(str(err))
+        s = pformat(query)
+
+    return s
+
+
+def formatOpenIDMessage(msg):
+    value_lists = {}
+    for (ns_uri, ns_key), value in msg.args.items():
+        l = value_lists.setdefault(ns_uri, {})
+        l[ns_key] = value
+
+    output = []
+
+    for ns_uri, values in value_lists.items():
+        ns_output = []
+
+        alias = msg.namespaces.getAlias(ns_uri)
+        if alias is message.NULL_NAMESPACE:
+            alias = 'openid'
+        ns_output.append("  %s <%s>" % (alias, ns_uri))
+
+        for key in OPENID_SORT_ORDER:
+            try:
+                ns_output.append("    %s = %s" % (key, values.pop(key)))
+            except KeyError:
+                pass
+
+        values = values.items()
+        values.sort()
+
+        for k, v in values:
+            ns_output.append("    %s = %s" % (k, v))
+
+        output.append('\n'.join(ns_output))
+
+    return '\n\n'.join(output)
+
+
+def unlistify(d):
+    return dict((i[0], i[1][0]) for i in d.items())
+
+
+def queriesFromLogs(s):
+    qre = re.compile(r'GET (/.*)?\?(.+) HTTP')
+
+    return [(match.group(1), cgi.parse_qs(match.group(2)))
+            for match in qre.finditer(s)]
+
+def queriesFromPostdata(s):
+    # This looks for query data in a line that starts POSTDATA=.
+    # Tamperdata outputs such lines.  If there's a 'Host=' in that block,
+    # use that too, but don't require it.
+    qre = re.compile(r'(?:^Host=(?P<host>.+?)$.*?)?^POSTDATA=(?P<query>.*)$',
+                     re.DOTALL | re.MULTILINE)
+    return [(match.group('host') or 'POSTDATA',
+             cgi.parse_qs(match.group('query'))) for match in qre.finditer(s)]
+
+def find_urls(s):
+    # Regular expression borrowed from urlscan
+    # by Daniel Burrows <dburrows@debian.org>, GPL.
+    urlinternalpattern=r'[{}a-zA-Z/\-_0-9%?&.=:;+,#~]'
+    urltrailingpattern=r'[{}a-zA-Z/\-_0-9%&=+#]'
+    httpurlpattern = r'(?:https?://' + urlinternalpattern + r'*' + urltrailingpattern + r')'
+    # Used to guess that blah.blah.blah.TLD is a URL.
+    tlds=['biz', 'com', 'edu', 'info', 'org']
+    guessedurlpattern=r'(?:[a-zA-Z0-9_\-%]+(?:\.[a-zA-Z0-9_\-%]+)*\.(?:' + '|'.join(tlds) + '))'
+    urlre = re.compile(r'(?:<(?:URL:)?)?(' + httpurlpattern + '|' + guessedurlpattern + '|(?:mailto:[a-zA-Z0-9\-_]*@[0-9a-zA-Z_\-.]*[0-9a-zA-Z_\-]))>?')
+
+    return [match.group(1) for match in urlre.finditer(s)]
+
+
+if __name__ == '__main__':
+    main()

+ 170 - 0
desktop/core/ext-py/python-openid-2.2.5/contrib/upgrade-store-1.1-to-2.0

@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# SQL Store Upgrade Script
+# for version 1.x to 2.0 of the OpenID library.
+# Doesn't depend on the openid library, so you can run this python
+# script to update databases for ruby or PHP as well.
+#
+# Testers note:
+#
+#   A SQLite3 db with the 1.2 schema exists in
+#   openid/test/data/openid-1.2-consumer-sqlitestore.db if you want something
+#   to try upgrading.
+#
+#   TODO:
+#    * test data for mysql and postgresql.
+#    * automated tests.
+
+import os
+import getpass
+import sys
+from optparse import OptionParser
+
+
+def askForPassword():
+    return getpass.getpass("DB Password: ")
+
+def askForConfirmation(dbname,tablename):
+    print """The table %s from the database %s will be dropped, and
+    an empty table with the new nonce table schema will replace it."""%(
+    tablename, dbname)
+    return raw_input("Continue? ").lower().strip().startswith('y')
+
+def doSQLiteUpgrade(db_conn, nonce_table_name='oid_nonces'):
+    cur = db_conn.cursor()
+    cur.execute('DROP TABLE %s'%nonce_table_name)
+    sql = """
+    CREATE TABLE %s (
+        server_url VARCHAR,
+        timestamp INTEGER,
+        salt CHAR(40),
+        UNIQUE(server_url, timestamp, salt)
+    );
+    """%nonce_table_name
+    cur.execute(sql)
+    cur.close()
+
+def doMySQLUpgrade(db_conn, nonce_table_name='oid_nonces'):
+    cur = db_conn.cursor()
+    cur.execute('DROP TABLE %s'%nonce_table_name)
+    sql = """
+    CREATE TABLE %s (
+        server_url BLOB,
+        timestamp INTEGER,
+        salt CHAR(40),
+        PRIMARY KEY (server_url(255), timestamp, salt)
+    )
+    TYPE=InnoDB;
+    """%nonce_table_name
+    cur.execute(sql)
+    cur.close()
+
+def doPostgreSQLUpgrade(db_conn, nonce_table_name='oid_nonces'):
+    cur = db_conn.cursor()
+    cur.execute('DROP TABLE %s'%nonce_table_name)
+    sql = """
+    CREATE TABLE %s (
+        server_url VARCHAR(2047),
+        timestamp INTEGER,
+        salt CHAR(40),
+        PRIMARY KEY (server_url, timestamp, salt)
+    );
+    """%nonce_table_name
+    cur.execute(sql)
+    cur.close()
+    db_conn.commit()
+
+def main(argv=None):
+    parser = OptionParser()
+    parser.add_option("-u", "--user", dest="username",
+                      default=os.environ.get('USER'),
+                      help="User name to use to connect to the DB.  "
+                      "Defaults to USER environment variable.")
+    parser.add_option('-t', '--table', dest='tablename', default='oid_nonces',
+                      help='The name of the nonce table to drop and recreate. '
+                      ' Defaults to "oid_nonces", the default table name for '
+                      'the openid stores.')
+    parser.add_option('--mysql', dest='mysql_db_name',
+                      help='Upgrade a table from this MySQL database.  '
+                      'Requires username for database.')
+    parser.add_option('--pg', '--postgresql', dest='postgres_db_name',
+                      help='Upgrade a table from this PostgreSQL database.  '
+                      'Requires username for database.')
+    parser.add_option('--sqlite', dest='sqlite_db_name',
+                      help='Upgrade a table from this SQLite database file.')
+    parser.add_option('--host', dest='db_host',
+                      default='localhost',
+                      help='Host on which to find MySQL or PostgreSQL DB.')
+    (options, args) = parser.parse_args(argv)
+
+    db_conn = None
+
+    if options.sqlite_db_name:
+        try:
+            from pysqlite2 import dbapi2 as sqlite
+        except ImportError:
+            print "You must have pysqlite2 installed in your PYTHONPATH."
+            return 1
+        try:
+            db_conn = sqlite.connect(options.sqlite_db_name)
+        except Exception, e:
+            print "Could not connect to SQLite database:", str(e)
+            return 1
+
+        if askForConfirmation(options.sqlite_db_name, options.tablename):
+            doSQLiteUpgrade(db_conn, nonce_table_name=options.tablename)
+
+    if options.postgres_db_name:
+        if not options.username:
+            print "A username is required to open a PostgreSQL Database."
+            return 1
+        password = askForPassword()
+        try:
+            import psycopg
+        except ImportError:
+            print "You need psycopg installed to update a postgres DB."
+            return 1
+
+        try:
+            db_conn = psycopg.connect(database = options.postgres_db_name,
+                                      user = options.username,
+                                      host = options.db_host,
+                                      password = password)
+        except Exception, e:
+            print "Could not connect to PostgreSQL database:", str(e)
+            return 1
+
+        if askForConfirmation(options.postgres_db_name, options.tablename):
+            doPostgreSQLUpgrade(db_conn, nonce_table_name=options.tablename)
+
+    if options.mysql_db_name:
+        if not options.username:
+            print "A username is required to open a MySQL Database."
+            return 1
+        password = askForPassword()
+        try:
+            import MySQLdb
+        except ImportError:
+            print "You must have MySQLdb installed to update a MySQL DB."
+            return 1
+
+        try:
+            db_conn = MySQLdb.connect(options.db_host, options.username,
+                                      password, options.mysql_db_name)
+        except Exception, e:
+            print "Could not connect to MySQL database:", str(e)
+            return 1
+
+        if askForConfirmation(options.mysql_db_name, options.tablename):
+            doMySQLUpgrade(db_conn, nonce_table_name=options.tablename)
+
+    if db_conn:
+        db_conn.close()
+    else:
+        parser.print_help()
+
+    return 0
+
+
+if __name__ == '__main__':
+    retval = main()
+    sys.exit(retval)

+ 91 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/README

@@ -0,0 +1,91 @@
+Python OpenID library example code
+==================================
+
+The examples directory contains working code illustrating the use of
+the library for performing OpenID authentication, both as a consumer
+and a server. There are two kinds of examples, one that can run
+without any external dependencies, and one that uses the Django Web
+framework. The examples do not illustrate how to use all of the
+features of the library, but they should be a good starting point to
+see how to use this library with your code.
+
+Both the Django libraries and the BaseHTTPServer examples require that
+the OpenID library is installed or that it has been added to Python's
+search path (PYTHONPATH environment variable or sys.path).
+
+The Django example is probably a good place to start reading the
+code. There is little that is Django-specific about the OpenID logic
+in the example, and it should be easy to port to any framework. To run
+the django examples, see the README file in the djopenid subdirectory.
+
+The other examples use Python's built-in BaseHTTPServer and have a
+good deal of ad-hoc dispatching and rendering code mixed in
+
+Using the BaseHTTPServer examples
+=================================
+
+This directory contains a working server and consumer that use this
+OpenID library. They are both written using python's standard
+BaseHTTPServer.
+
+
+To run the example system:
+
+1. Make sure you've installed the library, as explained in the
+   installation instructions.
+
+2. Start the consumer server:
+
+        python consumer.py --port 8001
+
+
+3. In another terminal, start the identity server:
+
+        python server.py --port 8000
+
+   (Hit Ctrl-C in either server's window to stop that server.)
+
+
+4. Open your web broswer, and go to the consumer server:
+
+        http://localhost:8001/
+
+   Note that all pages the consumer server shows will have "Python OpenID
+   Consumer Example" across the top.
+
+
+5. Enter an identity url managed by the sample identity server:
+
+        http://localhost:8000/id/bob
+
+
+6. The browser will be redirected to the sample server, which will be
+   requesting that you log in to proceed.  Enter the username for the
+   identity URL into the login box:
+
+        bob
+
+   Note that all pages the identity server shows will have "Python
+   OpenID Server Example" across the top.
+
+
+7. After you log in as bob, the server example will ask you if you
+   want to allow http://localhost:8001/ to know your identity.  Say
+   yes.
+
+
+8. You should end up back on the consumer site, at a page indicating
+   you've logged in successfully.
+
+
+That's a basic OpenID login procedure.  You can continue through it,
+playing with variations to see how they work.  The python code is
+intended to be a straightforward example of how to use the python
+OpenID library to function as either an identity server or consumer.
+
+Getting help
+============
+
+Please send bug reports, patches, and other feedback to
+
+  http://openid.net/developers/dev-mailing-lists/

+ 507 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/consumer.py

@@ -0,0 +1,507 @@
+#!/usr/bin/env python
+"""
+Simple example for an OpenID consumer.
+
+Once you understand this example you'll know the basics of OpenID
+and using the Python OpenID library. You can then move on to more
+robust examples, and integrating OpenID into your application.
+"""
+__copyright__ = 'Copyright 2005-2008, Janrain, Inc.'
+
+from Cookie import SimpleCookie
+import cgi
+import urlparse
+import cgitb
+import sys
+
+def quoteattr(s):
+    qs = cgi.escape(s, 1)
+    return '"%s"' % (qs,)
+
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
+try:
+    import openid
+except ImportError:
+    sys.stderr.write("""
+Failed to import the OpenID library. In order to use this example, you
+must either install the library (see INSTALL in the root of the
+distribution) or else add the library to python's import path (the
+PYTHONPATH environment variable).
+
+For more information, see the README in the root of the library
+distribution.""")
+    sys.exit(1)
+
+from openid.store import memstore
+from openid.store import filestore
+from openid.consumer import consumer
+from openid.oidutil import appendArgs
+from openid.cryptutil import randomString
+from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
+from openid.extensions import pape, sreg
+
+# Used with an OpenID provider affiliate program.
+OPENID_PROVIDER_NAME = 'MyOpenID'
+OPENID_PROVIDER_URL ='https://www.myopenid.com/affiliate_signup?affiliate_id=39'
+
+
+class OpenIDHTTPServer(HTTPServer):
+    """http server that contains a reference to an OpenID consumer and
+    knows its base URL.
+    """
+    def __init__(self, store, *args, **kwargs):
+        HTTPServer.__init__(self, *args, **kwargs)
+        self.sessions = {}
+        self.store = store
+
+        if self.server_port != 80:
+            self.base_url = ('http://%s:%s/' %
+                             (self.server_name, self.server_port))
+        else:
+            self.base_url = 'http://%s/' % (self.server_name,)
+
+class OpenIDRequestHandler(BaseHTTPRequestHandler):
+    """Request handler that knows how to verify an OpenID identity."""
+    SESSION_COOKIE_NAME = 'pyoidconsexsid'
+
+    session = None
+
+    def getConsumer(self, stateless=False):
+        if stateless:
+            store = None
+        else:
+            store = self.server.store
+        return consumer.Consumer(self.getSession(), store)
+
+    def getSession(self):
+        """Return the existing session or a new session"""
+        if self.session is not None:
+            return self.session
+
+        # Get value of cookie header that was sent
+        cookie_str = self.headers.get('Cookie')
+        if cookie_str:
+            cookie_obj = SimpleCookie(cookie_str)
+            sid_morsel = cookie_obj.get(self.SESSION_COOKIE_NAME, None)
+            if sid_morsel is not None:
+                sid = sid_morsel.value
+            else:
+                sid = None
+        else:
+            sid = None
+
+        # If a session id was not set, create a new one
+        if sid is None:
+            sid = randomString(16, '0123456789abcdef')
+            session = None
+        else:
+            session = self.server.sessions.get(sid)
+
+        # If no session exists for this session ID, create one
+        if session is None:
+            session = self.server.sessions[sid] = {}
+
+        session['id'] = sid
+        self.session = session
+        return session
+
+    def setSessionCookie(self):
+        sid = self.getSession()['id']
+        session_cookie = '%s=%s;' % (self.SESSION_COOKIE_NAME, sid)
+        self.send_header('Set-Cookie', session_cookie)
+
+    def do_GET(self):
+        """Dispatching logic. There are three paths defined:
+
+          / - Display an empty form asking for an identity URL to
+              verify
+          /verify - Handle form submission, initiating OpenID verification
+          /process - Handle a redirect from an OpenID server
+
+        Any other path gets a 404 response. This function also parses
+        the query parameters.
+
+        If an exception occurs in this function, a traceback is
+        written to the requesting browser.
+        """
+        try:
+            self.parsed_uri = urlparse.urlparse(self.path)
+            self.query = {}
+            for k, v in cgi.parse_qsl(self.parsed_uri[4]):
+                self.query[k] = v.decode('utf-8')
+
+            path = self.parsed_uri[2]
+            if path == '/':
+                self.render()
+            elif path == '/verify':
+                self.doVerify()
+            elif path == '/process':
+                self.doProcess()
+            elif path == '/affiliate':
+                self.doAffiliate()
+            else:
+                self.notFound()
+
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except:
+            self.send_response(500)
+            self.send_header('Content-type', 'text/html')
+            self.setSessionCookie()
+            self.end_headers()
+            self.wfile.write(cgitb.html(sys.exc_info(), context=10))
+
+    def doVerify(self):
+        """Process the form submission, initating OpenID verification.
+        """
+
+        # First, make sure that the user entered something
+        openid_url = self.query.get('openid_identifier')
+        if not openid_url:
+            self.render('Enter an OpenID Identifier to verify.',
+                        css_class='error', form_contents=openid_url)
+            return
+
+        immediate = 'immediate' in self.query
+        use_sreg = 'use_sreg' in self.query
+        use_pape = 'use_pape' in self.query
+        use_stateless = 'use_stateless' in self.query
+
+        oidconsumer = self.getConsumer(stateless = use_stateless)
+        try:
+            request = oidconsumer.begin(openid_url)
+        except consumer.DiscoveryFailure, exc:
+            fetch_error_string = 'Error in discovery: %s' % (
+                cgi.escape(str(exc[0])))
+            self.render(fetch_error_string,
+                        css_class='error',
+                        form_contents=openid_url)
+        else:
+            if request is None:
+                msg = 'No OpenID services found for <code>%s</code>' % (
+                    cgi.escape(openid_url),)
+                self.render(msg, css_class='error', form_contents=openid_url)
+            else:
+                # Then, ask the library to begin the authorization.
+                # Here we find out the identity server that will verify the
+                # user's identity, and get a token that allows us to
+                # communicate securely with the identity server.
+                if use_sreg:
+                    self.requestRegistrationData(request)
+
+                if use_pape:
+                    self.requestPAPEDetails(request)
+
+                trust_root = self.server.base_url
+                return_to = self.buildURL('process')
+                if request.shouldSendRedirect():
+                    redirect_url = request.redirectURL(
+                        trust_root, return_to, immediate=immediate)
+                    self.send_response(302)
+                    self.send_header('Location', redirect_url)
+                    self.writeUserHeader()
+                    self.end_headers()
+                else:
+                    form_html = request.htmlMarkup(
+                        trust_root, return_to,
+                        form_tag_attrs={'id':'openid_message'},
+                        immediate=immediate)
+
+                    self.wfile.write(form_html)
+
+    def requestRegistrationData(self, request):
+        sreg_request = sreg.SRegRequest(
+            required=['nickname'], optional=['fullname', 'email'])
+        request.addExtension(sreg_request)
+
+    def requestPAPEDetails(self, request):
+        pape_request = pape.Request([pape.AUTH_PHISHING_RESISTANT])
+        request.addExtension(pape_request)
+
+    def doProcess(self):
+        """Handle the redirect from the OpenID server.
+        """
+        oidconsumer = self.getConsumer()
+
+        # Ask the library to check the response that the server sent
+        # us.  Status is a code indicating the response type. info is
+        # either None or a string containing more information about
+        # the return type.
+        url = 'http://'+self.headers.get('Host')+self.path
+        info = oidconsumer.complete(self.query, url)
+
+        sreg_resp = None
+        pape_resp = None
+        css_class = 'error'
+        display_identifier = info.getDisplayIdentifier()
+
+        if info.status == consumer.FAILURE and display_identifier:
+            # In the case of failure, if info is non-None, it is the
+            # URL that we were verifying. We include it in the error
+            # message to help the user figure out what happened.
+            fmt = "Verification of %s failed: %s"
+            message = fmt % (cgi.escape(display_identifier),
+                             info.message)
+        elif info.status == consumer.SUCCESS:
+            # Success means that the transaction completed without
+            # error. If info is None, it means that the user cancelled
+            # the verification.
+            css_class = 'alert'
+
+            # This is a successful verification attempt. If this
+            # was a real application, we would do our login,
+            # comment posting, etc. here.
+            fmt = "You have successfully verified %s as your identity."
+            message = fmt % (cgi.escape(display_identifier),)
+            sreg_resp = sreg.SRegResponse.fromSuccessResponse(info)
+            pape_resp = pape.Response.fromSuccessResponse(info)
+            if info.endpoint.canonicalID:
+                # You should authorize i-name users by their canonicalID,
+                # rather than their more human-friendly identifiers.  That
+                # way their account with you is not compromised if their
+                # i-name registration expires and is bought by someone else.
+                message += ("  This is an i-name, and its persistent ID is %s"
+                            % (cgi.escape(info.endpoint.canonicalID),))
+        elif info.status == consumer.CANCEL:
+            # cancelled
+            message = 'Verification cancelled'
+        elif info.status == consumer.SETUP_NEEDED:
+            if info.setup_url:
+                message = '<a href=%s>Setup needed</a>' % (
+                    quoteattr(info.setup_url),)
+            else:
+                # This means auth didn't succeed, but you're welcome to try
+                # non-immediate mode.
+                message = 'Setup needed'
+        else:
+            # Either we don't understand the code or there is no
+            # openid_url included with the error. Give a generic
+            # failure message. The library should supply debug
+            # information in a log.
+            message = 'Verification failed.'
+
+        self.render(message, css_class, display_identifier,
+                    sreg_data=sreg_resp, pape_data=pape_resp)
+
+    def doAffiliate(self):
+        """Direct the user sign up with an affiliate OpenID provider."""
+        sreg_req = sreg.SRegRequest(['nickname'], ['fullname', 'email'])
+        href = sreg_req.toMessage().toURL(OPENID_PROVIDER_URL)
+
+        message = """Get an OpenID at <a href=%s>%s</a>""" % (
+            quoteattr(href), OPENID_PROVIDER_NAME)
+        self.render(message)
+
+    def renderSREG(self, sreg_data):
+        if not sreg_data:
+            self.wfile.write(
+                '<div class="alert">No registration data was returned</div>')
+        else:
+            sreg_list = sreg_data.items()
+            sreg_list.sort()
+            self.wfile.write(
+                '<h2>Registration Data</h2>'
+                '<table class="sreg">'
+                '<thead><tr><th>Field</th><th>Value</th></tr></thead>'
+                '<tbody>')
+
+            odd = ' class="odd"'
+            for k, v in sreg_list:
+                field_name = sreg.data_fields.get(k, k)
+                value = cgi.escape(v.encode('UTF-8'))
+                self.wfile.write(
+                    '<tr%s><td>%s</td><td>%s</td></tr>' % (odd, field_name, value))
+                if odd:
+                    odd = ''
+                else:
+                    odd = ' class="odd"'
+
+            self.wfile.write('</tbody></table>')
+
+    def renderPAPE(self, pape_data):
+        if not pape_data:
+            self.wfile.write(
+                '<div class="alert">No PAPE data was returned</div>')
+        else:
+            self.wfile.write('<div class="alert">Effective Auth Policies<ul>')
+
+            for policy_uri in pape_data.auth_policies:
+                self.wfile.write('<li><tt>%s</tt></li>' % (cgi.escape(policy_uri),))
+
+            if not pape_data.auth_policies:
+                self.wfile.write('<li>No policies were applied.</li>')
+
+            self.wfile.write('</ul></div>')
+
+    def buildURL(self, action, **query):
+        """Build a URL relative to the server base_url, with the given
+        query parameters added."""
+        base = urlparse.urljoin(self.server.base_url, action)
+        return appendArgs(base, query)
+
+    def notFound(self):
+        """Render a page with a 404 return code and a message."""
+        fmt = 'The path <q>%s</q> was not understood by this server.'
+        msg = fmt % (self.path,)
+        openid_url = self.query.get('openid_identifier')
+        self.render(msg, 'error', openid_url, status=404)
+
+    def render(self, message=None, css_class='alert', form_contents=None,
+               status=200, title="Python OpenID Consumer Example",
+               sreg_data=None, pape_data=None):
+        """Render a page."""
+        self.send_response(status)
+        self.pageHeader(title)
+        if message:
+            self.wfile.write("<div class='%s'>" % (css_class,))
+            self.wfile.write(message)
+            self.wfile.write("</div>")
+
+        if sreg_data is not None:
+            self.renderSREG(sreg_data)
+
+        if pape_data is not None:
+            self.renderPAPE(pape_data)
+
+        self.pageFooter(form_contents)
+
+    def pageHeader(self, title):
+        """Render the page header"""
+        self.setSessionCookie()
+        self.wfile.write('''\
+Content-type: text/html; charset=UTF-8
+
+<html>
+  <head><title>%s</title></head>
+  <style type="text/css">
+      * {
+        font-family: verdana,sans-serif;
+      }
+      body {
+        width: 50em;
+        margin: 1em;
+      }
+      div {
+        padding: .5em;
+      }
+      tr.odd td {
+        background-color: #dddddd;
+      }
+      table.sreg {
+        border: 1px solid black;
+        border-collapse: collapse;
+      }
+      table.sreg th {
+        border-bottom: 1px solid black;
+      }
+      table.sreg td, table.sreg th {
+        padding: 0.5em;
+        text-align: left;
+      }
+      table {
+        margin: 0;
+        padding: 0;
+      }
+      .alert {
+        border: 1px solid #e7dc2b;
+        background: #fff888;
+      }
+      .error {
+        border: 1px solid #ff0000;
+        background: #ffaaaa;
+      }
+      #verify-form {
+        border: 1px solid #777777;
+        background: #dddddd;
+        margin-top: 1em;
+        padding-bottom: 0em;
+      }
+  </style>
+  <body>
+    <h1>%s</h1>
+    <p>
+      This example consumer uses the <a href=
+      "http://github.com/openid/python-openid" >Python
+      OpenID</a> library. It just verifies that the identifier that you enter
+      is your identifier.
+    </p>
+''' % (title, title))
+
+    def pageFooter(self, form_contents):
+        """Render the page footer"""
+        if not form_contents:
+            form_contents = ''
+
+        self.wfile.write('''\
+    <div id="verify-form">
+      <form method="get" accept-charset="UTF-8" action=%s>
+        Identifier:
+        <input type="text" name="openid_identifier" value=%s />
+        <input type="submit" value="Verify" /><br />
+        <input type="checkbox" name="immediate" id="immediate" /><label for="immediate">Use immediate mode</label>
+        <input type="checkbox" name="use_sreg" id="use_sreg" /><label for="use_sreg">Request registration data</label>
+        <input type="checkbox" name="use_pape" id="use_pape" /><label for="use_pape">Request phishing-resistent auth policy (PAPE)</label>
+        <input type="checkbox" name="use_stateless" id="use_stateless" /><label for="use_stateless">Use stateless mode</label>
+      </form>
+    </div>
+  </body>
+</html>
+''' % (quoteattr(self.buildURL('verify')), quoteattr(form_contents)))
+
+def main(host, port, data_path, weak_ssl=False):
+    # Instantiate OpenID consumer store and OpenID consumer.  If you
+    # were connecting to a database, you would create the database
+    # connection and instantiate an appropriate store here.
+    if data_path:
+        store = filestore.FileOpenIDStore(data_path)
+    else:
+        store = memstore.MemoryStore()
+
+    if weak_ssl:
+        setDefaultFetcher(Urllib2Fetcher())
+
+    addr = (host, port)
+    server = OpenIDHTTPServer(store, addr, OpenIDRequestHandler)
+
+    print 'Server running at:'
+    print server.base_url
+    server.serve_forever()
+
+if __name__ == '__main__':
+    host = 'localhost'
+    port = 8001
+    weak_ssl = False
+
+    try:
+        import optparse
+    except ImportError:
+        pass # Use defaults (for Python 2.2)
+    else:
+        parser = optparse.OptionParser('Usage:\n %prog [options]')
+        parser.add_option(
+            '-d', '--data-path', dest='data_path',
+            help='Data directory for storing OpenID consumer state. '
+            'Setting this option implies using a "FileStore."')
+        parser.add_option(
+            '-p', '--port', dest='port', type='int', default=port,
+            help='Port on which to listen for HTTP requests. '
+            'Defaults to port %default.')
+        parser.add_option(
+            '-s', '--host', dest='host', default=host,
+            help='Host on which to listen for HTTP requests. '
+            'Also used for generating URLs. Defaults to %default.')
+        parser.add_option(
+            '-w', '--weakssl', dest='weakssl', default=False,
+            action='store_true', help='Skip ssl cert verification')
+
+        options, args = parser.parse_args()
+        if args:
+            parser.error('Expected no arguments. Got %r' % args)
+
+        host = options.host
+        port = options.port
+        data_path = options.data_path
+        weak_ssl = options.weakssl
+
+    main(host, port, data_path, weak_ssl)

+ 46 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/discover

@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+from openid.consumer.discover import discover, DiscoveryFailure
+from openid.fetchers import HTTPFetchingError
+
+names = [["server_url",  "Server URL  "],
+         ["local_id",    "Local ID    "],
+         ["canonicalID", "Canonical ID"],
+        ]
+
+def show_services(user_input, normalized, services):
+    print " Claimed identifier:", normalized
+    if services:
+        print " Discovered OpenID services:"
+        for n, service in enumerate(services):
+            print " %s." % (n,)
+            for attr, name in names:
+                val = getattr(service, attr, None)
+                if val is not None:
+                    print "  %s: %s" % (name, val)
+
+            print "  Type URIs:"
+            for type_uri in service.type_uris:
+                print "   *", type_uri
+
+            print
+
+    else:
+        print " No OpenID services found"
+        print
+
+if __name__ == "__main__":
+    import sys
+
+    for user_input in sys.argv[1:]:
+        print "=" * 50
+        print "Running discovery on", user_input
+        try:
+            normalized, services = discover(user_input)
+        except DiscoveryFailure, why:
+            print "Discovery failed:", why
+            print
+        except HTTPFetchingError, why:
+            print "HTTP request failed:", why
+            print
+        else:
+            show_services(user_input, normalized, services)

+ 67 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/README

@@ -0,0 +1,67 @@
+
+DJANGO EXAMPLE PACKAGE
+======================
+
+This package implements an example consumer and server for the Django
+Python web framework.  You can get Django (and learn more about it) at
+
+  http://www.djangoproject.com/
+
+SETUP
+=====
+
+ 1. Install the OpenID library, version 2.0.0 or later.
+
+ 2. Install Django 0.95.1.
+
+    If you find that the examples run on even newer versions of
+    Django, please let us know!
+
+ 3. Modify djopenid/settings.py appropriately; you may wish to change
+    the database type or path, although the default settings should be
+    sufficient for most systems.
+
+ 4. In examples/djopenid/ run:
+
+    python manage.py syncdb
+
+ 5. To run the example consumer or server, run
+
+    python manage.py runserver PORT
+
+    where PORT is the port number on which to listen.
+
+    Note that if you want to try both the consumer and server at the
+    same time, run the command twice with two different values for
+    PORT.
+
+ 6. Point your web browser at the server at
+
+    http://localhost:PORT/
+
+    to begin.
+
+ABOUT THE CODE
+==============
+
+The example server and consumer code provided in this package are
+intended to be instructional in the use of this OpenID library.  While
+it is not recommended to use the example code in production, the code
+should be sufficient to explain the general use of the library.
+
+If you aren't familiar with the Django web framework, you can quickly
+start looking at the important code by looking in the 'views' modules:
+
+  djopenid.consumer.views
+  djopenid.server.views
+
+Each view is a python callable that responds to an HTTP request.
+Regardless of whether you use a framework, your application should
+look similar to these example applications.
+
+CONTACT
+=======
+
+Please send bug reports, patches, and other feedback to
+
+  http://openid.net/developers/dev-mailing-lists/

+ 3 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/models.py

@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.

+ 9 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/urls.py

@@ -0,0 +1,9 @@
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns(
+    'djopenid.consumer.views',
+    (r'^$', 'startOpenID'),
+    (r'^finish/$', 'finishOpenID'),
+    (r'^xrds/$', 'rpXRDS'),
+)

+ 220 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/views.py

@@ -0,0 +1,220 @@
+
+from django import http
+from django.http import HttpResponseRedirect
+from django.views.generic.simple import direct_to_template
+
+from openid.consumer import consumer
+from openid.consumer.discover import DiscoveryFailure
+from openid.extensions import ax, pape, sreg
+from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE
+from openid.server.trustroot import RP_RETURN_TO_URL_TYPE
+
+from djopenid import util
+
+PAPE_POLICIES = [
+    'AUTH_PHISHING_RESISTANT',
+    'AUTH_MULTI_FACTOR',
+    'AUTH_MULTI_FACTOR_PHYSICAL',
+    ]
+
+# List of (name, uri) for use in generating the request form.
+POLICY_PAIRS = [(p, getattr(pape, p))
+                for p in PAPE_POLICIES]
+
+def getOpenIDStore():
+    """
+    Return an OpenID store object fit for the currently-chosen
+    database backend, if any.
+    """
+    return util.getOpenIDStore('/tmp/djopenid_c_store', 'c_')
+
+def getConsumer(request):
+    """
+    Get a Consumer object to perform OpenID authentication.
+    """
+    return consumer.Consumer(request.session, getOpenIDStore())
+
+def renderIndexPage(request, **template_args):
+    template_args['consumer_url'] = util.getViewURL(request, startOpenID)
+    template_args['pape_policies'] = POLICY_PAIRS
+
+    response =  direct_to_template(
+        request, 'consumer/index.html', template_args)
+    response[YADIS_HEADER_NAME] = util.getViewURL(request, rpXRDS)
+    return response
+
+def startOpenID(request):
+    """
+    Start the OpenID authentication process.  Renders an
+    authentication form and accepts its POST.
+
+    * Renders an error message if OpenID cannot be initiated
+
+    * Requests some Simple Registration data using the OpenID
+      library's Simple Registration machinery
+
+    * Generates the appropriate trust root and return URL values for
+      this application (tweak where appropriate)
+
+    * Generates the appropriate redirect based on the OpenID protocol
+      version.
+    """
+    if request.POST:
+        # Start OpenID authentication.
+        openid_url = request.POST['openid_identifier']
+        c = getConsumer(request)
+        error = None
+
+        try:
+            auth_request = c.begin(openid_url)
+        except DiscoveryFailure, e:
+            # Some other protocol-level failure occurred.
+            error = "OpenID discovery error: %s" % (str(e),)
+
+        if error:
+            # Render the page with an error.
+            return renderIndexPage(request, error=error)
+
+        # Add Simple Registration request information.  Some fields
+        # are optional, some are required.  It's possible that the
+        # server doesn't support sreg or won't return any of the
+        # fields.
+        sreg_request = sreg.SRegRequest(optional=['email', 'nickname'],
+                                        required=['dob'])
+        auth_request.addExtension(sreg_request)
+
+        # Add Attribute Exchange request information.
+        ax_request = ax.FetchRequest()
+        # XXX - uses myOpenID-compatible schema values, which are
+        # not those listed at axschema.org.
+        ax_request.add(
+            ax.AttrInfo('http://schema.openid.net/namePerson',
+                        required=True))
+        ax_request.add(
+            ax.AttrInfo('http://schema.openid.net/contact/web/default',
+                        required=False, count=ax.UNLIMITED_VALUES))
+        auth_request.addExtension(ax_request)
+
+        # Add PAPE request information.  We'll ask for
+        # phishing-resistant auth and display any policies we get in
+        # the response.
+        requested_policies = []
+        policy_prefix = 'policy_'
+        for k, v in request.POST.iteritems():
+            if k.startswith(policy_prefix):
+                policy_attr = k[len(policy_prefix):]
+                if policy_attr in PAPE_POLICIES:
+                    requested_policies.append(getattr(pape, policy_attr))
+
+        if requested_policies:
+            pape_request = pape.Request(requested_policies)
+            auth_request.addExtension(pape_request)
+
+        # Compute the trust root and return URL values to build the
+        # redirect information.
+        trust_root = util.getViewURL(request, startOpenID)
+        return_to = util.getViewURL(request, finishOpenID)
+
+        # Send the browser to the server either by sending a redirect
+        # URL or by generating a POST form.
+        if auth_request.shouldSendRedirect():
+            url = auth_request.redirectURL(trust_root, return_to)
+            return HttpResponseRedirect(url)
+        else:
+            # Beware: this renders a template whose content is a form
+            # and some javascript to submit it upon page load.  Non-JS
+            # users will have to click the form submit button to
+            # initiate OpenID authentication.
+            form_id = 'openid_message'
+            form_html = auth_request.formMarkup(trust_root, return_to,
+                                                False, {'id': form_id})
+            return direct_to_template(
+                request, 'consumer/request_form.html', {'html': form_html})
+
+    return renderIndexPage(request)
+
+def finishOpenID(request):
+    """
+    Finish the OpenID authentication process.  Invoke the OpenID
+    library with the response from the OpenID server and render a page
+    detailing the result.
+    """
+    result = {}
+
+    # Because the object containing the query parameters is a
+    # MultiValueDict and the OpenID library doesn't allow that, we'll
+    # convert it to a normal dict.
+
+    # OpenID 2 can send arguments as either POST body or GET query
+    # parameters.
+    request_args = util.normalDict(request.GET)
+    if request.method == 'POST':
+        request_args.update(util.normalDict(request.POST))
+
+    if request_args:
+        c = getConsumer(request)
+
+        # Get a response object indicating the result of the OpenID
+        # protocol.
+        return_to = util.getViewURL(request, finishOpenID)
+        response = c.complete(request_args, return_to)
+
+        # Get a Simple Registration response object if response
+        # information was included in the OpenID response.
+        sreg_response = {}
+        ax_items = {}
+        if response.status == consumer.SUCCESS:
+            sreg_response = sreg.SRegResponse.fromSuccessResponse(response)
+
+            ax_response = ax.FetchResponse.fromSuccessResponse(response)
+            if ax_response:
+                ax_items = {
+                    'fullname': ax_response.get(
+                        'http://schema.openid.net/namePerson'),
+                    'web': ax_response.get(
+                        'http://schema.openid.net/contact/web/default'),
+                    }
+
+        # Get a PAPE response object if response information was
+        # included in the OpenID response.
+        pape_response = None
+        if response.status == consumer.SUCCESS:
+            pape_response = pape.Response.fromSuccessResponse(response)
+
+            if not pape_response.auth_policies:
+                pape_response = None
+
+        # Map different consumer status codes to template contexts.
+        results = {
+            consumer.CANCEL:
+            {'message': 'OpenID authentication cancelled.'},
+
+            consumer.FAILURE:
+            {'error': 'OpenID authentication failed.'},
+
+            consumer.SUCCESS:
+            {'url': response.getDisplayIdentifier(),
+             'sreg': sreg_response and sreg_response.items(),
+             'ax': ax_items.items(),
+             'pape': pape_response}
+            }
+
+        result = results[response.status]
+
+        if isinstance(response, consumer.FailureResponse):
+            # In a real application, this information should be
+            # written to a log for debugging/tracking OpenID
+            # authentication failures. In general, the messages are
+            # not user-friendly, but intended for developers.
+            result['failure_reason'] = response.message
+
+    return renderIndexPage(request, **result)
+
+def rpXRDS(request):
+    """
+    Return a relying party verification XRDS document
+    """
+    return util.renderXRDS(
+        request,
+        [RP_RETURN_TO_URL_TYPE],
+        [util.getViewURL(request, finishOpenID)])

+ 11 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/manage.py

@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)

+ 3 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/models.py

@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.

+ 103 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/tests.py

@@ -0,0 +1,103 @@
+
+from django.test.testcases import TestCase
+from djopenid.server import views
+from djopenid import util
+
+from django.http import HttpRequest
+from django.contrib.sessions.middleware import SessionWrapper
+
+from openid.server.server import CheckIDRequest
+from openid.message import Message
+from openid.yadis.constants import YADIS_CONTENT_TYPE
+from openid.yadis.services import applyFilter
+
+def dummyRequest():
+    request = HttpRequest()
+    request.session = SessionWrapper("test")
+    request.META['HTTP_HOST'] = 'example.invalid'
+    request.META['SERVER_PROTOCOL'] = 'HTTP'
+    return request
+
+class TestProcessTrustResult(TestCase):
+    def setUp(self):
+        self.request = dummyRequest()
+
+        id_url = util.getViewURL(self.request, views.idPage)
+
+        # Set up the OpenID request we're responding to.
+        op_endpoint = 'http://127.0.0.1:8080/endpoint'
+        message = Message.fromPostArgs({
+            'openid.mode': 'checkid_setup',
+            'openid.identity': id_url,
+            'openid.return_to': 'http://127.0.0.1/%s' % (self.id(),),
+            'openid.sreg.required': 'postcode',
+            })
+        self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint)
+
+        views.setRequest(self.request, self.openid_request)
+
+
+    def test_allow(self):
+        self.request.POST['allow'] = 'Yes'
+
+        response = views.processTrustResult(self.request)
+
+        self.failUnlessEqual(response.status_code, 302)
+        finalURL = response['location']
+        self.failUnless('openid.mode=id_res' in finalURL, finalURL)
+        self.failUnless('openid.identity=' in finalURL, finalURL)
+        self.failUnless('openid.sreg.postcode=12345' in finalURL, finalURL)
+
+    def test_cancel(self):
+        self.request.POST['cancel'] = 'Yes'
+
+        response = views.processTrustResult(self.request)
+
+        self.failUnlessEqual(response.status_code, 302)
+        finalURL = response['location']
+        self.failUnless('openid.mode=cancel' in finalURL, finalURL)
+        self.failIf('openid.identity=' in finalURL, finalURL)
+        self.failIf('openid.sreg.postcode=12345' in finalURL, finalURL)
+
+
+
+class TestShowDecidePage(TestCase):
+    def test_unreachableRealm(self):
+        self.request = dummyRequest()
+
+        id_url = util.getViewURL(self.request, views.idPage)
+
+        # Set up the OpenID request we're responding to.
+        op_endpoint = 'http://127.0.0.1:8080/endpoint'
+        message = Message.fromPostArgs({
+            'openid.mode': 'checkid_setup',
+            'openid.identity': id_url,
+            'openid.return_to': 'http://unreachable.invalid/%s' % (self.id(),),
+            'openid.sreg.required': 'postcode',
+            })
+        self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint)
+
+        views.setRequest(self.request, self.openid_request)
+
+        response = views.showDecidePage(self.request, self.openid_request)
+        self.failUnless('trust_root_valid is Unreachable' in response.content,
+                        response)
+
+
+
+class TestGenericXRDS(TestCase):
+    def test_genericRender(self):
+        """Render an XRDS document with a single type URI and a single endpoint URL
+        Parse it to see that it matches."""
+        request = dummyRequest()
+
+        type_uris = ['A_TYPE']
+        endpoint_url = 'A_URL'
+        response = util.renderXRDS(request, type_uris, [endpoint_url])
+
+        requested_url = 'http://requested.invalid/'
+        (endpoint,) = applyFilter(requested_url, response.content)
+
+        self.failUnlessEqual(YADIS_CONTENT_TYPE, response['Content-Type'])
+        self.failUnlessEqual(type_uris, endpoint.type_uris)
+        self.failUnlessEqual(endpoint_url, endpoint.uri)

+ 12 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/urls.py

@@ -0,0 +1,12 @@
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns(
+    'djopenid.server.views',
+    (r'^$', 'server'),
+    (r'^xrds/$', 'idpXrds'),
+    (r'^processTrustResult/$', 'processTrustResult'),
+    (r'^user/$', 'idPage'),
+    (r'^endpoint/$', 'endpoint'),
+    (r'^trust/$', 'trustPage'),
+)

+ 279 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/views.py

@@ -0,0 +1,279 @@
+
+"""
+This module implements an example server for the OpenID library.  Some
+functionality has been omitted intentionally; this code is intended to
+be instructive on the use of this library.  This server does not
+perform actual user authentication and serves up only one OpenID URL,
+with the exception of IDP-generated identifiers.
+
+Some code conventions used here:
+
+* 'request' is a Django request object.
+
+* 'openid_request' is an OpenID library request object.
+
+* 'openid_response' is an OpenID library response
+"""
+
+import cgi
+
+from djopenid import util
+from djopenid.util import getViewURL
+
+from django import http
+from django.views.generic.simple import direct_to_template
+
+from openid.server.server import Server, ProtocolError, CheckIDRequest, \
+     EncodingError
+from openid.server.trustroot import verifyReturnTo
+from openid.yadis.discover import DiscoveryFailure
+from openid.consumer.discover import OPENID_IDP_2_0_TYPE
+from openid.extensions import sreg
+from openid.extensions import pape
+from openid.fetchers import HTTPFetchingError
+
+def getOpenIDStore():
+    """
+    Return an OpenID store object fit for the currently-chosen
+    database backend, if any.
+    """
+    return util.getOpenIDStore('/tmp/djopenid_s_store', 's_')
+
+def getServer(request):
+    """
+    Get a Server object to perform OpenID authentication.
+    """
+    return Server(getOpenIDStore(), getViewURL(request, endpoint))
+
+def setRequest(request, openid_request):
+    """
+    Store the openid request information in the session.
+    """
+    if openid_request:
+        request.session['openid_request'] = openid_request
+    else:
+        request.session['openid_request'] = None
+
+def getRequest(request):
+    """
+    Get an openid request from the session, if any.
+    """
+    return request.session.get('openid_request')
+
+def server(request):
+    """
+    Respond to requests for the server's primary web page.
+    """
+    return direct_to_template(
+        request,
+        'server/index.html',
+        {'user_url': getViewURL(request, idPage),
+         'server_xrds_url': getViewURL(request, idpXrds),
+         })
+
+def idpXrds(request):
+    """
+    Respond to requests for the IDP's XRDS document, which is used in
+    IDP-driven identifier selection.
+    """
+    return util.renderXRDS(
+        request, [OPENID_IDP_2_0_TYPE], [getViewURL(request, endpoint)])
+
+def idPage(request):
+    """
+    Serve the identity page for OpenID URLs.
+    """
+    return direct_to_template(
+        request,
+        'server/idPage.html',
+        {'server_url': getViewURL(request, endpoint)})
+
+def trustPage(request):
+    """
+    Display the trust page template, which allows the user to decide
+    whether to approve the OpenID verification.
+    """
+    return direct_to_template(
+        request,
+        'server/trust.html',
+        {'trust_handler_url':getViewURL(request, processTrustResult)})
+
+def endpoint(request):
+    """
+    Respond to low-level OpenID protocol messages.
+    """
+    s = getServer(request)
+
+    query = util.normalDict(request.GET or request.POST)
+
+    # First, decode the incoming request into something the OpenID
+    # library can use.
+    try:
+        openid_request = s.decodeRequest(query)
+    except ProtocolError, why:
+        # This means the incoming request was invalid.
+        return direct_to_template(
+            request,
+            'server/endpoint.html',
+            {'error': str(why)})
+
+    # If we did not get a request, display text indicating that this
+    # is an endpoint.
+    if openid_request is None:
+        return direct_to_template(
+            request,
+            'server/endpoint.html',
+            {})
+
+    # We got a request; if the mode is checkid_*, we will handle it by
+    # getting feedback from the user or by checking the session.
+    if openid_request.mode in ["checkid_immediate", "checkid_setup"]:
+        return handleCheckIDRequest(request, openid_request)
+    else:
+        # We got some other kind of OpenID request, so we let the
+        # server handle this.
+        openid_response = s.handleRequest(openid_request)
+        return displayResponse(request, openid_response)
+
+def handleCheckIDRequest(request, openid_request):
+    """
+    Handle checkid_* requests.  Get input from the user to find out
+    whether she trusts the RP involved.  Possibly, get intput about
+    what Simple Registration information, if any, to send in the
+    response.
+    """
+    # If the request was an IDP-driven identifier selection request
+    # (i.e., the IDP URL was entered at the RP), then return the
+    # default identity URL for this server. In a full-featured
+    # provider, there could be interaction with the user to determine
+    # what URL should be sent.
+    if not openid_request.idSelect():
+
+        id_url = getViewURL(request, idPage)
+
+        # Confirm that this server can actually vouch for that
+        # identifier
+        if id_url != openid_request.identity:
+            # Return an error response
+            error_response = ProtocolError(
+                openid_request.message,
+                "This server cannot verify the URL %r" %
+                (openid_request.identity,))
+
+            return displayResponse(request, error_response)
+
+    if openid_request.immediate:
+        # Always respond with 'cancel' to immediate mode requests
+        # because we don't track information about a logged-in user.
+        # If we did, then the answer would depend on whether that user
+        # had trusted the request's trust root and whether the user is
+        # even logged in.
+        openid_response = openid_request.answer(False)
+        return displayResponse(request, openid_response)
+    else:
+        # Store the incoming request object in the session so we can
+        # get to it later.
+        setRequest(request, openid_request)
+        return showDecidePage(request, openid_request)
+
+def showDecidePage(request, openid_request):
+    """
+    Render a page to the user so a trust decision can be made.
+
+    @type openid_request: openid.server.server.CheckIDRequest
+    """
+    trust_root = openid_request.trust_root
+    return_to = openid_request.return_to
+
+    try:
+        # Stringify because template's ifequal can only compare to strings.
+        trust_root_valid = verifyReturnTo(trust_root, return_to) \
+                           and "Valid" or "Invalid"
+    except DiscoveryFailure, err:
+        trust_root_valid = "DISCOVERY_FAILED"
+    except HTTPFetchingError, err:
+        trust_root_valid = "Unreachable"
+
+    pape_request = pape.Request.fromOpenIDRequest(openid_request)
+
+    return direct_to_template(
+        request,
+        'server/trust.html',
+        {'trust_root': trust_root,
+         'trust_handler_url':getViewURL(request, processTrustResult),
+         'trust_root_valid': trust_root_valid,
+         'pape_request': pape_request,
+         })
+
+def processTrustResult(request):
+    """
+    Handle the result of a trust decision and respond to the RP
+    accordingly.
+    """
+    # Get the request from the session so we can construct the
+    # appropriate response.
+    openid_request = getRequest(request)
+
+    # The identifier that this server can vouch for
+    response_identity = getViewURL(request, idPage)
+
+    # If the decision was to allow the verification, respond
+    # accordingly.
+    allowed = 'allow' in request.POST
+
+    # Generate a response with the appropriate answer.
+    openid_response = openid_request.answer(allowed,
+                                            identity=response_identity)
+
+    # Send Simple Registration data in the response, if appropriate.
+    if allowed:
+        sreg_data = {
+            'fullname': 'Example User',
+            'nickname': 'example',
+            'dob': '1970-01-01',
+            'email': 'invalid@example.com',
+            'gender': 'F',
+            'postcode': '12345',
+            'country': 'ES',
+            'language': 'eu',
+            'timezone': 'America/New_York',
+            }
+
+        sreg_req = sreg.SRegRequest.fromOpenIDRequest(openid_request)
+        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
+        openid_response.addExtension(sreg_resp)
+
+        pape_response = pape.Response()
+        pape_response.setAuthLevel(pape.LEVELS_NIST, 0)
+        openid_response.addExtension(pape_response)
+
+    return displayResponse(request, openid_response)
+
+def displayResponse(request, openid_response):
+    """
+    Display an OpenID response.  Errors will be displayed directly to
+    the user; successful responses and other protocol-level messages
+    will be sent using the proper mechanism (i.e., direct response,
+    redirection, etc.).
+    """
+    s = getServer(request)
+
+    # Encode the response into something that is renderable.
+    try:
+        webresponse = s.encodeResponse(openid_response)
+    except EncodingError, why:
+        # If it couldn't be encoded, display an error.
+        text = why.response.encodeToKVForm()
+        return direct_to_template(
+            request,
+            'server/endpoint.html',
+            {'error': cgi.escape(text)})
+
+    # Construct the appropriate django framework response.
+    r = http.HttpResponse(webresponse.body)
+    r.status_code = webresponse.code
+
+    for header, value in webresponse.headers.iteritems():
+        r[header] = value
+
+    return r

+ 84 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/settings.py

@@ -0,0 +1,84 @@
+# Django settings for djopenid project.
+
+import os
+import sys
+import warnings
+
+try:
+    import openid
+except ImportError, e:
+    warnings.warn("Could not import OpenID library.  Please consult the djopenid README.")
+    sys.exit(1)
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3'           # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
+DATABASE_NAME = '/tmp/test.db'             # Or path to database file if using sqlite3.
+DATABASE_USER = ''             # Not used with sqlite3.
+DATABASE_PASSWORD = ''         # Not used with sqlite3.
+DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. All choices can be found here:
+# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+# http://blogs.law.harvard.edu/tech/stories/storyReader$15
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT.
+# Example: "http://media.lawrence.com"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'u^bw6lmsa6fah0$^lz-ct$)y7x7#ag92-z+y45-8!(jk0lkavy'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+#     'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'djopenid.urls'
+
+TEMPLATE_CONTEXT_PROCESSORS = ()
+
+TEMPLATE_DIRS = (
+    os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates')),
+)
+
+INSTALLED_APPS = (
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+
+    'djopenid.consumer',
+    'djopenid.server',
+)

+ 127 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/index.html

@@ -0,0 +1,127 @@
+<html>
+  <head>
+    <title>Django OpenID Example Consumer</title>
+    <style type="text/css">
+    div.message {
+        background: #9f9;
+        padding: 0.5em;
+        margin-top: 0.5em;
+        margin-bottom: 0.5em;
+        border: 1px solid #555;
+        font-weight: bold;
+    }
+
+    div.error {
+        background: #f99;
+        padding: 0.5em;
+        margin-top: 0.5em;
+        margin-bottom: 0.5em;
+        border: 1px solid #555;
+        font-weight: bold;
+    }
+
+    div.box {
+        background: #eee;
+        border: 1px solid black;
+        padding: 1em;
+    }
+    </style>
+  </head>
+  <body>
+
+  <div class="box">
+
+  <p>
+    This is an example consumer built for the Django framework.  Enter
+    an OpenID in the box below.
+  </p>
+
+  {% if error %}
+    <div class="error">{{ error|escape }}</div>
+  {% endif %}
+
+  {% if url %}
+    <div class="message">
+      OpenID authentication succeeded; you authenticated as
+        <a href="{{ url }}">{{ url|escape }}</a>.
+
+      <p>
+      {% if sreg %}
+        Simple Registration data returned:
+
+        <ul>
+        {% for pair in sreg %}
+          <li>{{ pair.0 }}: {{ pair.1 }}</li>
+        {% endfor %}
+        </ul>
+      {% else %}
+        The server returned no Simple Registration data.
+      {% endif %}
+
+      {% if ax %}
+        Attribute Exchange data returned:
+
+        <ul>
+        {% for pair in ax %}
+        <li>{{ pair.0 }}: {{ pair.1|join:", " }}</li>
+        {% endfor %}
+        </ul>
+      {% else %}
+        The server returned no Attribute Exchange data.
+      {% endif %}
+
+      {% if pape %}
+        An authentication policy response contained these policies:
+
+        <ul>
+          {% for uri in pape.auth_policies %}
+            <li><tt>{{ uri }}</tt></li>
+          {% endfor %}
+        </ul>
+      {% else %}
+        The server returned no authentication policy data (PAPE).
+      {% endif %}
+      </p>
+    </div>
+  {% endif %}
+
+  {% if message %}
+    <div class="message">
+      {{ message|escape }}
+    </div>
+  {% endif %}
+
+  {% if failure_reason %}
+    <div class="error">
+      {{ failure_reason|escape }}
+    </div>
+  {% endif %}
+
+  <form method="post" action="{{ consumer_url }}">
+    <input type="text" size="40" name="openid_identifier" />
+
+    <p>
+    Request these authentication policies
+      (<a href="http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-02.html">PAPE</a>):
+
+    <table>
+      {% for pair in pape_policies %}
+      <tr>
+        <td><input type="checkbox" name="policy_{{ pair.0 }}" id="id_policy_{{ pair.0 }}" /></td>
+        <td>
+          <label for="id_policy_{{ pair.0 }}"><tt>
+            {{ pair.1 }}
+          </tt></label>
+        </td>
+      </tr>
+      {% endfor %}
+    </table>
+    </p>
+
+    <input type="submit" value="Begin" />
+  </form>
+
+  </div>
+
+  </body>
+</html>

+ 5 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/request_form.html

@@ -0,0 +1,5 @@
+<html>
+  <body onload="document.getElementById('openid_message').submit()">
+  {{ html }}
+  </body>
+</html>

+ 28 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/index.html

@@ -0,0 +1,28 @@
+<html>
+  <head>
+    <title>Python OpenID Example</title>
+  </head>
+  <body>
+    <p>
+    This is a Django package which implements both an OpenID server
+    and an OpenID consumer.  These examples are provided with the
+    OpenID library so you can learn how to use it in your own
+    applications.
+    </p>
+
+    <p>
+    To begin, click one of these links:
+    </p>
+
+    <ul>
+      <li><a href="{{ consumer_url }}">Example Consumer (Relying Party)</a></li>
+      <li><a href="{{ server_url }}">Example Server (Identity Provider)</a></li>
+    </ul>
+
+    <p>
+    <strong>Note:</strong> If you want to test the example consumer
+    using the example server, you must start a separate server process
+    for each application.
+    </p>
+  </body>
+</html>

+ 15 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/endpoint.html

@@ -0,0 +1,15 @@
+<html>
+  <body>
+
+    This is an OpenID server endpoint.  Your browser should never
+    actually request this page.
+
+    {% if error %}
+      The OpenID server has encountered an error:
+      <p>
+      {{ error|escape }}
+      </p>
+    {% endif %}
+
+  </body>
+</html>

+ 14 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/idPage.html

@@ -0,0 +1,14 @@
+{% extends "server/index.html" %}
+
+{% block head %}
+<!-- We should be able to space-separate both rel values in one attribute,
+     but some older OpenID implementations don't parse that. -->
+<link rel="openid.server" href="{{ server_url }}">
+<link rel="openid2.provider" href="{{ server_url }}">
+{% endblock %}
+
+{% block body %}
+<p>
+This is the identity page for the OpenID that this server serves.
+</p>
+{% endblock %}

+ 51 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/index.html

@@ -0,0 +1,51 @@
+<html>
+  <head>
+    <title>Django OpenID Example Server</title>
+    <style type="text/css">
+    div.message {
+        background: #9f9;
+        padding: 0.5em;
+        margin-top: 0.5em;
+        margin-bottom: 0.5em;
+        border: 1px solid #555;
+        font-weight: bold;
+    }
+
+    div.error {
+        background: #f99;
+        padding: 0.5em;
+        margin-top: 0.5em;
+        margin-bottom: 0.5em;
+        border: 1px solid #555;
+        font-weight: bold;
+    }
+
+    div.box {
+        background: #eee;
+        border: 1px solid black;
+        padding: 1em;
+    }
+    </style>
+    {% block head %}
+    <meta http-equiv="x-xrds-location" content="{{ server_xrds_url }}">
+    {% endblock %}
+  </head>
+  <body>
+
+{% block body %}
+  <div class="box">
+
+  <p>
+    This is an example server built for the Django framework.  It only
+    authenticates one OpenID, which is also served by this
+    application.  The OpenID it serves is
+
+    <pre>
+<a href="{{ user_url }}">{{ user_url }}</a>
+    </pre>
+  </p>
+
+  </div>
+{% endblock %}
+  </body>
+</html>

+ 21 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/pape_request_info.html

@@ -0,0 +1,21 @@
+{% if pape_request %}
+  {% if pape_request.preferred_auth_policies %}
+    The relying party requested the following PAPE policies be in effect:
+
+    <ul>
+    {% for uri in pape_request.preferred_auth_policies %}
+    <li>{{ uri }}</li>
+    {% endfor %}
+    </ul>
+  {% endif %}
+
+  {% if pape_request.preferred_auth_level_types %}
+    The relying party requested the following authentication level types:
+
+    <ul>
+    {% for uri in pape_request.preferred_auth_level_types %}
+    <li>{{ uri }}</li>
+    {% endfor %}
+    </ul>
+  {% endif %}
+{% endif %}

+ 51 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/trust.html

@@ -0,0 +1,51 @@
+{% extends "server/index.html" %}
+
+{% block body %}
+
+{% ifequal trust_root_valid "Valid" %}
+  <!-- Trust root has been validated by OpenID 2 mechanism. -->
+  <p>The site <tt>{{ trust_root|escape }}</tt> has requested verification
+  of your OpenID.</p>
+
+  {% include "server/pape_request_info.html" %}
+{% endifequal %}
+{% ifequal trust_root_valid "Invalid" %}
+<div class="error">
+  <p>This request claims to be from {{ trust_root|escape }} but I have 
+  determined that <em>it is a pack of lies</em>.  Beware, if you release
+  information to them, they are likely to do unconscionable things with it,
+  being the lying liars that they are.</p>
+  <p>Please tell the <em>real</em> {{ trust_root|escape }} that someone is
+  trying to abuse your trust in their good name.</p>
+</div>
+{% endifequal %}
+{% ifequal trust_root_valid "Unreachable" %}
+  <p>The site <tt>{{ trust_root|escape }}</tt> has requested verification
+  of your OpenID.  I have failed to reach it and thus cannot vouch for its
+  authenticity.  Perhaps it is on your local network.</p>
+{% endifequal %}
+{% ifequal trust_root_valid "DISCOVERY_FAILED" %}
+  <p>The site <tt>{{ trust_root|escape }}</tt> has requested verification
+  of your OpenID.  However, <tt>{{ trust_root|escape }}</tt> does not 
+  implement OpenID 2.0's relying party verification mechanism.  Please use
+  extra caution in deciding whether to release information to this party,
+  and ask <tt>{{ trust_root|escape }}</tt> to implement relying party
+  verification for your future transactions.</p>
+
+  {% include "server/pape_request_info.html" %}
+{% endifequal %}
+
+<!-- trust_root_valid is {{ trust_root_valid }} -->
+
+<div>
+<form method="post"
+      action="{{ trust_handler_url }}">
+  Verify your identity to the relying party?
+
+  <br/>
+  <input type="submit" value="Yes (Allow)" name="allow" />
+  <input type="submit" value="No (Cancel)" name="cancel" />
+</form>
+</div>
+
+{% endblock %}

+ 17 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/xrds.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xrds:XRDS
+    xmlns:xrds="xri://$xrds"
+    xmlns="xri://$xrd*($v*2.0)">
+  <XRD>
+
+    <Service priority="0">
+    {% for type_uri in type_uris %}
+      <Type>{{ type_uri|escape }}</Type>
+    {% endfor %}
+    {% for endpoint_url in endpoint_urls %}
+      <URI>{{ endpoint_url }}</URI>
+    {% endfor %}
+    </Service>
+
+  </XRD>
+</xrds:XRDS>

+ 8 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/urls.py

@@ -0,0 +1,8 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns(
+    '',
+    ('^$', 'djopenid.views.index'),
+    ('^consumer/', include('djopenid.consumer.urls')),
+    ('^server/', include('djopenid.server.urls')),
+)

+ 147 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/util.py

@@ -0,0 +1,147 @@
+
+"""
+Utility code for the Django example consumer and server.
+"""
+
+from urlparse import urljoin
+
+from django.db import connection
+from django.template.context import RequestContext
+from django.template import loader
+from django import http
+from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import reverse as reverseURL
+from django.views.generic.simple import direct_to_template
+
+from django.conf import settings
+
+from openid.store.filestore import FileOpenIDStore
+from openid.store import sqlstore
+from openid.yadis.constants import YADIS_CONTENT_TYPE
+
+def getOpenIDStore(filestore_path, table_prefix):
+    """
+    Returns an OpenID association store object based on the database
+    engine chosen for this Django application.
+
+    * If no database engine is chosen, a filesystem-based store will
+      be used whose path is filestore_path.
+
+    * If a database engine is chosen, a store object for that database
+      type will be returned.
+
+    * If the chosen engine is not supported by the OpenID library,
+      raise ImproperlyConfigured.
+
+    * If a database store is used, this will create the tables
+      necessary to use it.  The table names will be prefixed with
+      table_prefix.  DO NOT use the same table prefix for both an
+      OpenID consumer and an OpenID server in the same database.
+
+    The result of this function should be passed to the Consumer
+    constructor as the store parameter.
+    """
+    if not settings.DATABASE_ENGINE:
+        return FileOpenIDStore(filestore_path)
+
+    # Possible side-effect: create a database connection if one isn't
+    # already open.
+    connection.cursor()
+
+    # Create table names to specify for SQL-backed stores.
+    tablenames = {
+        'associations_table': table_prefix + 'openid_associations',
+        'nonces_table': table_prefix + 'openid_nonces',
+        }
+
+    types = {
+        'postgresql': sqlstore.PostgreSQLStore,
+        'mysql': sqlstore.MySQLStore,
+        'sqlite3': sqlstore.SQLiteStore,
+        }
+
+    try:
+        s = types[settings.DATABASE_ENGINE](connection.connection,
+                                            **tablenames)
+    except KeyError:
+        raise ImproperlyConfigured, \
+              "Database engine %s not supported by OpenID library" % \
+              (settings.DATABASE_ENGINE,)
+
+    try:
+        s.createTables()
+    except (SystemExit, KeyboardInterrupt, MemoryError), e:
+        raise
+    except:
+        # XXX This is not the Right Way to do this, but because the
+        # underlying database implementation might differ in behavior
+        # at this point, we can't reliably catch the right
+        # exception(s) here.  Ideally, the SQL store in the OpenID
+        # library would catch exceptions that it expects and fail
+        # silently, but that could be bad, too.  More ideally, the SQL
+        # store would not attempt to create tables it knows already
+        # exists.
+        pass
+
+    return s
+
+def getViewURL(req, view_name_or_obj, args=None, kwargs=None):
+    relative_url = reverseURL(view_name_or_obj, args=args, kwargs=kwargs)
+    full_path = req.META.get('SCRIPT_NAME', '') + relative_url
+    return urljoin(getBaseURL(req), full_path)
+
+def getBaseURL(req):
+    """
+    Given a Django web request object, returns the OpenID 'trust root'
+    for that request; namely, the absolute URL to the site root which
+    is serving the Django request.  The trust root will include the
+    proper scheme and authority.  It will lack a port if the port is
+    standard (80, 443).
+    """
+    name = req.META['HTTP_HOST']
+    try:
+        name = name[:name.index(':')]
+    except:
+        pass
+
+    try:
+        port = int(req.META['SERVER_PORT'])
+    except:
+        port = 80
+
+    proto = req.META['SERVER_PROTOCOL']
+
+    if 'HTTPS' in proto:
+        proto = 'https'
+    else:
+        proto = 'http'
+
+    if port in [80, 443] or not port:
+        port = ''
+    else:
+        port = ':%s' % (port,)
+
+    url = "%s://%s%s/" % (proto, name, port)
+    return url
+
+def normalDict(request_data):
+    """
+    Converts a django request MutliValueDict (e.g., request.GET,
+    request.POST) into a standard python dict whose values are the
+    first value from each of the MultiValueDict's value lists.  This
+    avoids the OpenID library's refusal to deal with dicts whose
+    values are lists, because in OpenID, each key in the query arg set
+    can have at most one value.
+    """
+    return dict((k, v[0]) for k, v in request_data.iteritems())
+
+def renderXRDS(request, type_uris, endpoint_urls):
+    """Render an XRDS page with the specified type URIs and endpoint
+    URLs in one service block, and return a response with the
+    appropriate content-type.
+    """
+    response = direct_to_template(
+        request, 'xrds.xml',
+        {'type_uris':type_uris, 'endpoint_urls':endpoint_urls,})
+    response['Content-Type'] = YADIS_CONTENT_TYPE
+    return response

+ 14 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/views.py

@@ -0,0 +1,14 @@
+
+from djopenid import util
+from django.views.generic.simple import direct_to_template
+
+def index(request):
+    consumer_url = util.getViewURL(
+        request, 'djopenid.consumer.views.startOpenID')
+    server_url = util.getViewURL(request, 'djopenid.server.views.server')
+
+    return direct_to_template(
+        request,
+        'index.html',
+        {'consumer_url':consumer_url, 'server_url':server_url})
+

+ 721 - 0
desktop/core/ext-py/python-openid-2.2.5/examples/server.py

@@ -0,0 +1,721 @@
+#!/usr/bin/env python
+
+__copyright__ = 'Copyright 2005-2008, Janrain, Inc.'
+
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+from urlparse import urlparse
+
+import time
+import Cookie
+import cgi
+import cgitb
+import sys
+
+def quoteattr(s):
+    qs = cgi.escape(s, 1)
+    return '"%s"' % (qs,)
+
+try:
+    import openid
+except ImportError:
+    sys.stderr.write("""
+Failed to import the OpenID library. In order to use this example, you
+must either install the library (see INSTALL in the root of the
+distribution) or else add the library to python's import path (the
+PYTHONPATH environment variable).
+
+For more information, see the README in the root of the library
+distribution.""")
+    sys.exit(1)
+
+from openid.extensions import sreg
+from openid.server import server
+from openid.store.filestore import FileOpenIDStore
+from openid.consumer import discover
+
+class OpenIDHTTPServer(HTTPServer):
+    """
+    http server that contains a reference to an OpenID Server and
+    knows its base URL.
+    """
+    def __init__(self, *args, **kwargs):
+        HTTPServer.__init__(self, *args, **kwargs)
+
+        if self.server_port != 80:
+            self.base_url = ('http://%s:%s/' %
+                             (self.server_name, self.server_port))
+        else:
+            self.base_url = 'http://%s/' % (self.server_name,)
+
+        self.openid = None
+        self.approved = {}
+        self.lastCheckIDRequest = {}
+
+    def setOpenIDServer(self, oidserver):
+        self.openid = oidserver
+
+
+class ServerHandler(BaseHTTPRequestHandler):
+    def __init__(self, *args, **kwargs):
+        self.user = None
+        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
+
+
+    def do_GET(self):
+        try:
+            self.parsed_uri = urlparse(self.path)
+            self.query = {}
+            for k, v in cgi.parse_qsl(self.parsed_uri[4]):
+                self.query[k] = v
+
+            self.setUser()
+
+            path = self.parsed_uri[2].lower()
+
+            if path == '/':
+                self.showMainPage()
+            elif path == '/openidserver':
+                self.serverEndPoint(self.query)
+
+            elif path == '/login':
+                self.showLoginPage('/', '/')
+            elif path == '/loginsubmit':
+                self.doLogin()
+            elif path.startswith('/id/'):
+                self.showIdPage(path)
+            elif path.startswith('/yadis/'):
+                self.showYadis(path[7:])
+            elif path == '/serveryadis':
+                self.showServerYadis()
+            else:
+                self.send_response(404)
+                self.end_headers()
+
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except:
+            self.send_response(500)
+            self.send_header('Content-type', 'text/html')
+            self.end_headers()
+            self.wfile.write(cgitb.html(sys.exc_info(), context=10))
+
+    def do_POST(self):
+        try:
+            self.parsed_uri = urlparse(self.path)
+
+            self.setUser()
+            content_length = int(self.headers['Content-Length'])
+            post_data = self.rfile.read(content_length)
+
+            self.query = {}
+            for k, v in cgi.parse_qsl(post_data):
+                self.query[k] = v
+
+            path = self.parsed_uri[2]
+            if path == '/openidserver':
+                self.serverEndPoint(self.query)
+
+            elif path == '/allow':
+                self.handleAllow(self.query)
+            else:
+                self.send_response(404)
+                self.end_headers()
+
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except:
+            self.send_response(500)
+            self.send_header('Content-type', 'text/html')
+            self.end_headers()
+            self.wfile.write(cgitb.html(sys.exc_info(), context=10))
+
+    def handleAllow(self, query):
+        # pretend this next bit is keying off the user's session or something,
+        # right?
+        request = self.server.lastCheckIDRequest.get(self.user)
+
+        if 'yes' in query:
+            if 'login_as' in query:
+                self.user = self.query['login_as']
+
+            if request.idSelect():
+                identity = self.server.base_url + 'id/' + query['identifier']
+            else:
+                identity = request.identity
+
+            trust_root = request.trust_root
+            if self.query.get('remember', 'no') == 'yes':
+                self.server.approved[(identity, trust_root)] = 'always'
+
+            response = self.approved(request, identity)
+
+        elif 'no' in query:
+            response = request.answer(False)
+
+        else:
+            assert False, 'strange allow post.  %r' % (query,)
+
+        self.displayResponse(response)
+
+
+    def setUser(self):
+        cookies = self.headers.get('Cookie')
+        if cookies:
+            morsel = Cookie.BaseCookie(cookies).get('user')
+            if morsel:
+                self.user = morsel.value
+
+    def isAuthorized(self, identity_url, trust_root):
+        if self.user is None:
+            return False
+
+        if identity_url != self.server.base_url + 'id/' + self.user:
+            return False
+
+        key = (identity_url, trust_root)
+        return self.server.approved.get(key) is not None
+
+    def serverEndPoint(self, query):
+        try:
+            request = self.server.openid.decodeRequest(query)
+        except server.ProtocolError, why:
+            self.displayResponse(why)
+            return
+
+        if request is None:
+            # Display text indicating that this is an endpoint.
+            self.showAboutPage()
+            return
+
+        if request.mode in ["checkid_immediate", "checkid_setup"]:
+            self.handleCheckIDRequest(request)
+        else:
+            response = self.server.openid.handleRequest(request)
+            self.displayResponse(response)
+
+    def addSRegResponse(self, request, response):
+        sreg_req = sreg.SRegRequest.fromOpenIDRequest(request)
+
+        # In a real application, this data would be user-specific,
+        # and the user should be asked for permission to release
+        # it.
+        sreg_data = {
+            'nickname':self.user
+            }
+
+        sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
+        response.addExtension(sreg_resp)
+
+    def approved(self, request, identifier=None):
+        response = request.answer(True, identity=identifier)
+        self.addSRegResponse(request, response)
+        return response
+
+    def handleCheckIDRequest(self, request):
+        is_authorized = self.isAuthorized(request.identity, request.trust_root)
+        if is_authorized:
+            response = self.approved(request)
+            self.displayResponse(response)
+        elif request.immediate:
+            response = request.answer(False)
+            self.displayResponse(response)
+        else:
+            self.server.lastCheckIDRequest[self.user] = request
+            self.showDecidePage(request)
+
+    def displayResponse(self, response):
+        try:
+            webresponse = self.server.openid.encodeResponse(response)
+        except server.EncodingError, why:
+            text = why.response.encodeToKVForm()
+            self.showErrorPage('<pre>%s</pre>' % cgi.escape(text))
+            return
+
+        self.send_response(webresponse.code)
+        for header, value in webresponse.headers.iteritems():
+            self.send_header(header, value)
+        self.writeUserHeader()
+        self.end_headers()
+
+        if webresponse.body:
+            self.wfile.write(webresponse.body)
+
+    def doLogin(self):
+        if 'submit' in self.query:
+            if 'user' in self.query:
+                self.user = self.query['user']
+            else:
+                self.user = None
+            self.redirect(self.query['success_to'])
+        elif 'cancel' in self.query:
+            self.redirect(self.query['fail_to'])
+        else:
+            assert 0, 'strange login %r' % (self.query,)
+
+    def redirect(self, url):
+        self.send_response(302)
+        self.send_header('Location', url)
+        self.writeUserHeader()
+
+        self.end_headers()
+
+    def writeUserHeader(self):
+        if self.user is None:
+            t1970 = time.gmtime(0)
+            expires = time.strftime(
+                'Expires=%a, %d-%b-%y %H:%M:%S GMT', t1970)
+            self.send_header('Set-Cookie', 'user=;%s' % expires)
+        else:
+            self.send_header('Set-Cookie', 'user=%s' % self.user)
+
+    def showAboutPage(self):
+        endpoint_url = self.server.base_url + 'openidserver'
+
+        def link(url):
+            url_attr = quoteattr(url)
+            url_text = cgi.escape(url)
+            return '<a href=%s><code>%s</code></a>' % (url_attr, url_text)
+
+        def term(url, text):
+            return '<dt>%s</dt><dd>%s</dd>' % (link(url), text)
+
+        resources = [
+            (self.server.base_url, "This example server's home page"),
+            ('http://www.openidenabled.com/',
+             'An OpenID community Web site, home of this library'),
+            ('http://www.openid.net/', 'the official OpenID Web site'),
+            ]
+
+        resource_markup = ''.join([term(url, text) for url, text in resources])
+
+        self.showPage(200, 'This is an OpenID server', msg="""\
+        <p>%s is an OpenID server endpoint.<p>
+        <p>For more information about OpenID, see:</p>
+        <dl>
+        %s
+        </dl>
+        """ % (link(endpoint_url), resource_markup,))
+
+    def showErrorPage(self, error_message):
+        self.showPage(400, 'Error Processing Request', err='''\
+        <p>%s</p>
+        <!--
+
+        This is a large comment.  It exists to make this page larger.
+        That is unfortunately necessary because of the "smart"
+        handling of pages returned with an error code in IE.
+
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+        *************************************************************
+
+        -->
+        ''' % error_message)
+
+    def showDecidePage(self, request):
+        id_url_base = self.server.base_url+'id/'
+        # XXX: This may break if there are any synonyms for id_url_base,
+        # such as referring to it by IP address or a CNAME.
+        assert (request.identity.startswith(id_url_base) or 
+                request.idSelect()), repr((request.identity, id_url_base))
+        expected_user = request.identity[len(id_url_base):]
+
+        if request.idSelect(): # We are being asked to select an ID
+            msg = '''\
+            <p>A site has asked for your identity.  You may select an
+            identifier by which you would like this site to know you.
+            On a production site this would likely be a drop down list
+            of pre-created accounts or have the facility to generate
+            a random anonymous identifier.
+            </p>
+            '''
+            fdata = {
+                'id_url_base': id_url_base,
+                'trust_root': request.trust_root,
+                }
+            form = '''\
+            <form method="POST" action="/allow">
+            <table>
+              <tr><td>Identity:</td>
+                 <td>%(id_url_base)s<input type='text' name='identifier'></td></tr>
+              <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
+            </table>
+            <p>Allow this authentication to proceed?</p>
+            <input type="checkbox" id="remember" name="remember" value="yes"
+                /><label for="remember">Remember this
+                decision</label><br />
+            <input type="submit" name="yes" value="yes" />
+            <input type="submit" name="no" value="no" />
+            </form>
+            '''%fdata
+        elif expected_user == self.user:
+            msg = '''\
+            <p>A new site has asked to confirm your identity.  If you
+            approve, the site represented by the trust root below will
+            be told that you control identity URL listed below. (If
+            you are using a delegated identity, the site will take
+            care of reversing the delegation on its own.)</p>'''
+
+            fdata = {
+                'identity': request.identity,
+                'trust_root': request.trust_root,
+                }
+            form = '''\
+            <table>
+              <tr><td>Identity:</td><td>%(identity)s</td></tr>
+              <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
+            </table>
+            <p>Allow this authentication to proceed?</p>
+            <form method="POST" action="/allow">
+              <input type="checkbox" id="remember" name="remember" value="yes"
+                  /><label for="remember">Remember this
+                  decision</label><br />
+              <input type="submit" name="yes" value="yes" />
+              <input type="submit" name="no" value="no" />
+            </form>''' % fdata
+        else:
+            mdata = {
+                'expected_user': expected_user,
+                'user': self.user,
+                }
+            msg = '''\
+            <p>A site has asked for an identity belonging to
+            %(expected_user)s, but you are logged in as %(user)s.  To
+            log in as %(expected_user)s and approve the login request,
+            hit OK below.  The "Remember this decision" checkbox
+            applies only to the trust root decision.</p>''' % mdata
+
+            fdata = {
+                'identity': request.identity,
+                'trust_root': request.trust_root,
+                'expected_user': expected_user,
+                }
+            form = '''\
+            <table>
+              <tr><td>Identity:</td><td>%(identity)s</td></tr>
+              <tr><td>Trust Root:</td><td>%(trust_root)s</td></tr>
+            </table>
+            <p>Allow this authentication to proceed?</p>
+            <form method="POST" action="/allow">
+              <input type="checkbox" id="remember" name="remember" value="yes"
+                  /><label for="remember">Remember this
+                  decision</label><br />
+              <input type="hidden" name="login_as" value="%(expected_user)s"/>
+              <input type="submit" name="yes" value="yes" />
+              <input type="submit" name="no" value="no" />
+            </form>''' % fdata
+
+        self.showPage(200, 'Approve OpenID request?', msg=msg, form=form)
+
+    def showIdPage(self, path):
+        link_tag = '<link rel="openid.server" href="%sopenidserver">' %\
+              self.server.base_url
+        yadis_loc_tag = '<meta http-equiv="x-xrds-location" content="%s">'%\
+            (self.server.base_url+'yadis/'+path[4:])
+        disco_tags = link_tag + yadis_loc_tag
+        ident = self.server.base_url + path[1:]
+
+        approved_trust_roots = []
+        for (aident, trust_root) in self.server.approved.keys():
+            if aident == ident:
+                trs = '<li><tt>%s</tt></li>\n' % cgi.escape(trust_root)
+                approved_trust_roots.append(trs)
+
+        if approved_trust_roots:
+            prepend = '<p>Approved trust roots:</p>\n<ul>\n'
+            approved_trust_roots.insert(0, prepend)
+            approved_trust_roots.append('</ul>\n')
+            msg = ''.join(approved_trust_roots)
+        else:
+            msg = ''
+
+        self.showPage(200, 'An Identity Page', head_extras=disco_tags, msg='''\
+        <p>This is an identity page for %s.</p>
+        %s
+        ''' % (ident, msg))
+
+    def showYadis(self, user):
+        self.send_response(200)
+        self.send_header('Content-type', 'application/xrds+xml')
+        self.end_headers()
+
+        endpoint_url = self.server.base_url + 'openidserver'
+        user_url = self.server.base_url + 'id/' + user
+        self.wfile.write("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<xrds:XRDS
+    xmlns:xrds="xri://$xrds"
+    xmlns="xri://$xrd*($v*2.0)">
+  <XRD>
+
+    <Service priority="0">
+      <Type>%s</Type>
+      <Type>%s</Type>
+      <URI>%s</URI>
+      <LocalID>%s</LocalID>
+    </Service>
+
+  </XRD>
+</xrds:XRDS>
+"""%(discover.OPENID_2_0_TYPE, discover.OPENID_1_0_TYPE,
+     endpoint_url, user_url))
+
+    def showServerYadis(self):
+        self.send_response(200)
+        self.send_header('Content-type', 'application/xrds+xml')
+        self.end_headers()
+
+        endpoint_url = self.server.base_url + 'openidserver'
+        self.wfile.write("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<xrds:XRDS
+    xmlns:xrds="xri://$xrds"
+    xmlns="xri://$xrd*($v*2.0)">
+  <XRD>
+
+    <Service priority="0">
+      <Type>%s</Type>
+      <URI>%s</URI>
+    </Service>
+
+  </XRD>
+</xrds:XRDS>
+"""%(discover.OPENID_IDP_2_0_TYPE, endpoint_url,))
+
+    def showMainPage(self):
+        yadis_tag = '<meta http-equiv="x-xrds-location" content="%s">'%\
+            (self.server.base_url + 'serveryadis')
+        if self.user:
+            openid_url = self.server.base_url + 'id/' + self.user
+            user_message = """\
+            <p>You are logged in as %s. Your OpenID identity URL is
+            <tt><a href=%s>%s</a></tt>. Enter that URL at an OpenID
+            consumer to test this server.</p>
+            """ % (self.user, quoteattr(openid_url), openid_url)
+        else:
+            user_message = """\
+            <p>This server uses a cookie to remember who you are in
+            order to simulate a standard Web user experience. You are
+            not <a href='/login'>logged in</a>.</p>"""
+
+        self.showPage(200, 'Main Page', head_extras = yadis_tag, msg='''\
+        <p>This is a simple OpenID server implemented using the <a
+        href="http://openid.schtuff.com/">Python OpenID
+        library</a>.</p>
+
+        %s
+
+        <p>To use this server with a consumer, the consumer must be
+        able to fetch HTTP pages from this web server. If this
+        computer is behind a firewall, you will not be able to use
+        OpenID consumers outside of the firewall with it.</p>
+
+        <p>The URL for this server is <a href=%s><tt>%s</tt></a>.</p>
+        ''' % (user_message, quoteattr(self.server.base_url), self.server.base_url))
+
+    def showLoginPage(self, success_to, fail_to):
+        self.showPage(200, 'Login Page', form='''\
+        <h2>Login</h2>
+        <p>You may log in with any name. This server does not use
+        passwords because it is just a sample of how to use the OpenID
+        library.</p>
+        <form method="GET" action="/loginsubmit">
+          <input type="hidden" name="success_to" value="%s" />
+          <input type="hidden" name="fail_to" value="%s" />
+          <input type="text" name="user" value="" />
+          <input type="submit" name="submit" value="Log In" />
+          <input type="submit" name="cancel" value="Cancel" />
+        </form>
+        ''' % (success_to, fail_to))
+
+    def showPage(self, response_code, title,
+                 head_extras='', msg=None, err=None, form=None):
+
+        if self.user is None:
+            user_link = '<a href="/login">not logged in</a>.'
+        else:
+            user_link = 'logged in as <a href="/id/%s">%s</a>.<br /><a href="/loginsubmit?submit=true&success_to=/login">Log out</a>' % \
+                        (self.user, self.user)
+
+        body = ''
+
+        if err is not None:
+            body +=  '''\
+            <div class="error">
+              %s
+            </div>
+            ''' % err
+
+        if msg is not None:
+            body += '''\
+            <div class="message">
+              %s
+            </div>
+            ''' % msg
+
+        if form is not None:
+            body += '''\
+            <div class="form">
+              %s
+            </div>
+            ''' % form
+
+        contents = {
+            'title': 'Python OpenID Server Example - ' + title,
+            'head_extras': head_extras,
+            'body': body,
+            'user_link': user_link,
+            }
+
+        self.send_response(response_code)
+        self.writeUserHeader()
+        self.send_header('Content-type', 'text/html')
+        self.end_headers()
+
+        self.wfile.write('''<html>
+  <head>
+    <title>%(title)s</title>
+    %(head_extras)s
+  </head>
+  <style type="text/css">
+      h1 a:link {
+          color: black;
+          text-decoration: none;
+      }
+      h1 a:visited {
+          color: black;
+          text-decoration: none;
+      }
+      h1 a:hover {
+          text-decoration: underline;
+      }
+      body {
+        font-family: verdana,sans-serif;
+        width: 50em;
+        margin: 1em;
+      }
+      div {
+        padding: .5em;
+      }
+      table {
+        margin: none;
+        padding: none;
+      }
+      .banner {
+        padding: none 1em 1em 1em;
+        width: 100%%;
+      }
+      .leftbanner {
+        text-align: left;
+      }
+      .rightbanner {
+        text-align: right;
+        font-size: smaller;
+      }
+      .error {
+        border: 1px solid #ff0000;
+        background: #ffaaaa;
+        margin: .5em;
+      }
+      .message {
+        border: 1px solid #2233ff;
+        background: #eeeeff;
+        margin: .5em;
+      }
+      .form {
+        border: 1px solid #777777;
+        background: #ddddcc;
+        margin: .5em;
+        margin-top: 1em;
+        padding-bottom: 0em;
+      }
+      dd {
+        margin-bottom: 0.5em;
+      }
+  </style>
+  <body>
+    <table class="banner">
+      <tr>
+        <td class="leftbanner">
+          <h1><a href="/">Python OpenID Server Example</a></h1>
+        </td>
+        <td class="rightbanner">
+          You are %(user_link)s
+        </td>
+      </tr>
+    </table>
+%(body)s
+  </body>
+</html>
+''' % contents)
+
+
+def main(host, port, data_path):
+    addr = (host, port)
+    httpserver = OpenIDHTTPServer(addr, ServerHandler)
+
+    # Instantiate OpenID consumer store and OpenID consumer.  If you
+    # were connecting to a database, you would create the database
+    # connection and instantiate an appropriate store here.
+    store = FileOpenIDStore(data_path)
+    oidserver = server.Server(store, httpserver.base_url + 'openidserver')
+
+    httpserver.setOpenIDServer(oidserver)
+
+    print 'Server running at:'
+    print httpserver.base_url
+    httpserver.serve_forever()
+
+if __name__ == '__main__':
+    host = 'localhost'
+    data_path = 'sstore'
+    port = 8000
+
+    try:
+        import optparse
+    except ImportError:
+        pass # Use defaults (for Python 2.2)
+    else:
+        parser = optparse.OptionParser('Usage:\n %prog [options]')
+        parser.add_option(
+            '-d', '--data-path', dest='data_path', default=data_path,
+            help='Data directory for storing OpenID consumer state. '
+            'Defaults to "%default" in the current directory.')
+        parser.add_option(
+            '-p', '--port', dest='port', type='int', default=port,
+            help='Port on which to listen for HTTP requests. '
+            'Defaults to port %default.')
+        parser.add_option(
+            '-s', '--host', dest='host', default=host,
+            help='Host on which to listen for HTTP requests. '
+            'Also used for generating URLs. Defaults to %default.')
+
+        options, args = parser.parse_args()
+        if args:
+            parser.error('Expected no arguments. Got %r' % args)
+
+        host = options.host
+        port = options.port
+        data_path = options.data_path
+
+    main(host, port, data_path)

+ 55 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/__init__.py

@@ -0,0 +1,55 @@
+"""
+This package is an implementation of the OpenID specification in
+Python.  It contains code for both server and consumer
+implementations.  For information on implementing an OpenID consumer,
+see the C{L{openid.consumer.consumer}} module.  For information on
+implementing an OpenID server, see the C{L{openid.server.server}}
+module.
+
+@contact: U{http://openid.net/developers/dev-mailing-lists/
+    <http://openid.net/developers/dev-mailing-lists/}
+
+@copyright: (C) 2005-2008 JanRain, Inc.
+
+@license: Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+    U{http://www.apache.org/licenses/LICENSE-2.0}
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions
+    and limitations under the License.
+"""
+
+__version__ = '[library version:2.2.1]'[17:-1]
+
+__all__ = [
+    'association',
+    'consumer',
+    'cryptutil',
+    'dh',
+    'extension',
+    'extensions',
+    'fetchers',
+    'kvform',
+    'message',
+    'oidutil',
+    'server',
+    'sreg',
+    'store',
+    'urinorm',
+    'yadis',
+    ]
+
+# Parse the version info
+try:
+    version_info = map(int, __version__.split('.'))
+except ValueError:
+    version_info = (None, None, None)
+else:
+    if len(version_info) != 3:
+        version_info = (None, None, None)
+    else:
+        version_info = tuple(version_info)

+ 555 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/association.py

@@ -0,0 +1,555 @@
+# -*- test-case-name: openid.test.test_association -*-
+"""
+This module contains code for dealing with associations between
+consumers and servers. Associations contain a shared secret that is
+used to sign C{openid.mode=id_res} messages.
+
+Users of the library should not usually need to interact directly with
+associations. The L{store<openid.store>},
+L{server<openid.server.server>} and
+L{consumer<openid.consumer.consumer>} objects will create and manage
+the associations. The consumer and server code will make use of a
+C{L{SessionNegotiator}} when managing associations, which enables
+users to express a preference for what kind of associations should be
+allowed, and what kind of exchange should be done to establish the
+association.
+
+@var default_negotiator: A C{L{SessionNegotiator}} that allows all
+    association types that are specified by the OpenID
+    specification. It prefers to use HMAC-SHA1/DH-SHA1, if it's
+    available. If HMAC-SHA256 is not supported by your Python runtime,
+    HMAC-SHA256 and DH-SHA256 will not be available.
+
+@var encrypted_negotiator: A C{L{SessionNegotiator}} that
+    does not support C{'no-encryption'} associations. It prefers
+    HMAC-SHA1/DH-SHA1 association types if available.
+"""
+
+__all__ = [
+    'default_negotiator',
+    'encrypted_negotiator',
+    'SessionNegotiator',
+    'Association',
+    ]
+
+import time
+
+from openid import cryptutil
+from openid import kvform
+from openid import oidutil
+from openid.message import OPENID_NS
+
+all_association_types = [
+    'HMAC-SHA1',
+    'HMAC-SHA256',
+    ]
+
+if hasattr(cryptutil, 'hmacSha256'):
+    supported_association_types = list(all_association_types)
+
+    default_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ('HMAC-SHA1', 'no-encryption'),
+        ('HMAC-SHA256', 'DH-SHA256'),
+        ('HMAC-SHA256', 'no-encryption'),
+        ]
+
+    only_encrypted_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ('HMAC-SHA256', 'DH-SHA256'),
+        ]
+else:
+    supported_association_types = ['HMAC-SHA1']
+
+    default_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ('HMAC-SHA1', 'no-encryption'),
+        ]
+
+    only_encrypted_association_order = [
+        ('HMAC-SHA1', 'DH-SHA1'),
+        ]
+
+def getSessionTypes(assoc_type):
+    """Return the allowed session types for a given association type"""
+    assoc_to_session = {
+        'HMAC-SHA1': ['DH-SHA1', 'no-encryption'],
+        'HMAC-SHA256': ['DH-SHA256', 'no-encryption'],
+        }
+    return assoc_to_session.get(assoc_type, [])
+
+def checkSessionType(assoc_type, session_type):
+    """Check to make sure that this pair of assoc type and session
+    type are allowed"""
+    if session_type not in getSessionTypes(assoc_type):
+        raise ValueError(
+            'Session type %r not valid for assocation type %r'
+            % (session_type, assoc_type))
+
+class SessionNegotiator(object):
+    """A session negotiator controls the allowed and preferred
+    association types and association session types. Both the
+    C{L{Consumer<openid.consumer.consumer.Consumer>}} and
+    C{L{Server<openid.server.server.Server>}} use negotiators when
+    creating associations.
+
+    You can create and use negotiators if you:
+
+     - Do not want to do Diffie-Hellman key exchange because you use
+       transport-layer encryption (e.g. SSL)
+
+     - Want to use only SHA-256 associations
+
+     - Do not want to support plain-text associations over a non-secure
+       channel
+
+    It is up to you to set a policy for what kinds of associations to
+    accept. By default, the library will make any kind of association
+    that is allowed in the OpenID 2.0 specification.
+
+    Use of negotiators in the library
+    =================================
+
+    When a consumer makes an association request, it calls
+    C{L{getAllowedType}} to get the preferred association type and
+    association session type.
+
+    The server gets a request for a particular association/session
+    type and calls C{L{isAllowed}} to determine if it should
+    create an association. If it is supported, negotiation is
+    complete. If it is not, the server calls C{L{getAllowedType}} to
+    get an allowed association type to return to the consumer.
+
+    If the consumer gets an error response indicating that the
+    requested association/session type is not supported by the server
+    that contains an assocation/session type to try, it calls
+    C{L{isAllowed}} to determine if it should try again with the
+    given combination of association/session type.
+
+    @ivar allowed_types: A list of association/session types that are
+        allowed by the server. The order of the pairs in this list
+        determines preference. If an association/session type comes
+        earlier in the list, the library is more likely to use that
+        type.
+    @type allowed_types: [(str, str)]
+    """
+
+    def __init__(self, allowed_types):
+        self.setAllowedTypes(allowed_types)
+
+    def copy(self):
+        return self.__class__(list(self.allowed_types))
+
+    def setAllowedTypes(self, allowed_types):
+        """Set the allowed association types, checking to make sure
+        each combination is valid."""
+        for (assoc_type, session_type) in allowed_types:
+            checkSessionType(assoc_type, session_type)
+
+        self.allowed_types = allowed_types
+
+    def addAllowedType(self, assoc_type, session_type=None):
+        """Add an association type and session type to the allowed
+        types list. The assocation/session pairs are tried in the
+        order that they are added."""
+        if self.allowed_types is None:
+            self.allowed_types = []
+
+        if session_type is None:
+            available = getSessionTypes(assoc_type)
+
+            if not available:
+                raise ValueError('No session available for association type %r'
+                                 % (assoc_type,))
+
+            for session_type in getSessionTypes(assoc_type):
+                self.addAllowedType(assoc_type, session_type)
+        else:
+            checkSessionType(assoc_type, session_type)
+            self.allowed_types.append((assoc_type, session_type))
+
+
+    def isAllowed(self, assoc_type, session_type):
+        """Is this combination of association type and session type allowed?"""
+        assoc_good = (assoc_type, session_type) in self.allowed_types
+        matches = session_type in getSessionTypes(assoc_type)
+        return assoc_good and matches
+
+    def getAllowedType(self):
+        """Get a pair of assocation type and session type that are
+        supported"""
+        try:
+            return self.allowed_types[0]
+        except IndexError:
+            return (None, None)
+
+default_negotiator = SessionNegotiator(default_association_order)
+encrypted_negotiator = SessionNegotiator(only_encrypted_association_order)
+
+def getSecretSize(assoc_type):
+    if assoc_type == 'HMAC-SHA1':
+        return 20
+    elif assoc_type == 'HMAC-SHA256':
+        return 32
+    else:
+        raise ValueError('Unsupported association type: %r' % (assoc_type,))
+
+class Association(object):
+    """
+    This class represents an association between a server and a
+    consumer.  In general, users of this library will never see
+    instances of this object.  The only exception is if you implement
+    a custom C{L{OpenIDStore<openid.store.interface.OpenIDStore>}}.
+
+    If you do implement such a store, it will need to store the values
+    of the C{L{handle}}, C{L{secret}}, C{L{issued}}, C{L{lifetime}}, and
+    C{L{assoc_type}} instance variables.
+
+    @ivar handle: This is the handle the server gave this association.
+
+    @type handle: C{str}
+
+
+    @ivar secret: This is the shared secret the server generated for
+        this association.
+
+    @type secret: C{str}
+
+
+    @ivar issued: This is the time this association was issued, in
+        seconds since 00:00 GMT, January 1, 1970.  (ie, a unix
+        timestamp)
+
+    @type issued: C{int}
+
+
+    @ivar lifetime: This is the amount of time this association is
+        good for, measured in seconds since the association was
+        issued.
+
+    @type lifetime: C{int}
+
+
+    @ivar assoc_type: This is the type of association this instance
+        represents.  The only valid value of this field at this time
+        is C{'HMAC-SHA1'}, but new types may be defined in the future.
+
+    @type assoc_type: C{str}
+
+
+    @sort: __init__, fromExpiresIn, getExpiresIn, __eq__, __ne__,
+        handle, secret, issued, lifetime, assoc_type
+    """
+
+    # The ordering and name of keys as stored by serialize
+    assoc_keys = [
+        'version',
+        'handle',
+        'secret',
+        'issued',
+        'lifetime',
+        'assoc_type',
+        ]
+
+
+    _macs = {
+        'HMAC-SHA1': cryptutil.hmacSha1,
+        'HMAC-SHA256': cryptutil.hmacSha256,
+        }
+
+
+    def fromExpiresIn(cls, expires_in, handle, secret, assoc_type):
+        """
+        This is an alternate constructor used by the OpenID consumer
+        library to create associations.  C{L{OpenIDStore
+        <openid.store.interface.OpenIDStore>}} implementations
+        shouldn't use this constructor.
+
+
+        @param expires_in: This is the amount of time this association
+            is good for, measured in seconds since the association was
+            issued.
+
+        @type expires_in: C{int}
+
+
+        @param handle: This is the handle the server gave this
+            association.
+
+        @type handle: C{str}
+
+
+        @param secret: This is the shared secret the server generated
+            for this association.
+
+        @type secret: C{str}
+
+
+        @param assoc_type: This is the type of association this
+            instance represents.  The only valid value of this field
+            at this time is C{'HMAC-SHA1'}, but new types may be
+            defined in the future.
+
+        @type assoc_type: C{str}
+        """
+        issued = int(time.time())
+        lifetime = expires_in
+        return cls(handle, secret, issued, lifetime, assoc_type)
+
+    fromExpiresIn = classmethod(fromExpiresIn)
+
+    def __init__(self, handle, secret, issued, lifetime, assoc_type):
+        """
+        This is the standard constructor for creating an association.
+
+
+        @param handle: This is the handle the server gave this
+            association.
+
+        @type handle: C{str}
+
+
+        @param secret: This is the shared secret the server generated
+            for this association.
+
+        @type secret: C{str}
+
+
+        @param issued: This is the time this association was issued,
+            in seconds since 00:00 GMT, January 1, 1970.  (ie, a unix
+            timestamp)
+
+        @type issued: C{int}
+
+
+        @param lifetime: This is the amount of time this association
+            is good for, measured in seconds since the association was
+            issued.
+
+        @type lifetime: C{int}
+
+
+        @param assoc_type: This is the type of association this
+            instance represents.  The only valid value of this field
+            at this time is C{'HMAC-SHA1'}, but new types may be
+            defined in the future.
+
+        @type assoc_type: C{str}
+        """
+        if assoc_type not in all_association_types:
+            fmt = '%r is not a supported association type'
+            raise ValueError(fmt % (assoc_type,))
+
+#         secret_size = getSecretSize(assoc_type)
+#         if len(secret) != secret_size:
+#             fmt = 'Wrong size secret (%s bytes) for association type %s'
+#             raise ValueError(fmt % (len(secret), assoc_type))
+
+        self.handle = handle
+        self.secret = secret
+        self.issued = issued
+        self.lifetime = lifetime
+        self.assoc_type = assoc_type
+
+    def getExpiresIn(self, now=None):
+        """
+        This returns the number of seconds this association is still
+        valid for, or C{0} if the association is no longer valid.
+
+
+        @return: The number of seconds this association is still valid
+            for, or C{0} if the association is no longer valid.
+
+        @rtype: C{int}
+        """
+        if now is None:
+            now = int(time.time())
+
+        return max(0, self.issued + self.lifetime - now)
+
+    expiresIn = property(getExpiresIn)
+
+    def __eq__(self, other):
+        """
+        This checks to see if two C{L{Association}} instances
+        represent the same association.
+
+
+        @return: C{True} if the two instances represent the same
+            association, C{False} otherwise.
+
+        @rtype: C{bool}
+        """
+        return type(self) is type(other) and self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        """
+        This checks to see if two C{L{Association}} instances
+        represent different associations.
+
+
+        @return: C{True} if the two instances represent different
+            associations, C{False} otherwise.
+
+        @rtype: C{bool}
+        """
+        return not (self == other)
+
+    def serialize(self):
+        """
+        Convert an association to KV form.
+
+        @return: String in KV form suitable for deserialization by
+            deserialize.
+
+        @rtype: str
+        """
+        data = {
+            'version':'2',
+            'handle':self.handle,
+            'secret':oidutil.toBase64(self.secret),
+            'issued':str(int(self.issued)),
+            'lifetime':str(int(self.lifetime)),
+            'assoc_type':self.assoc_type
+            }
+
+        assert len(data) == len(self.assoc_keys)
+        pairs = []
+        for field_name in self.assoc_keys:
+            pairs.append((field_name, data[field_name]))
+
+        return kvform.seqToKV(pairs, strict=True)
+
+    def deserialize(cls, assoc_s):
+        """
+        Parse an association as stored by serialize().
+
+        inverse of serialize
+
+
+        @param assoc_s: Association as serialized by serialize()
+
+        @type assoc_s: str
+
+
+        @return: instance of this class
+        """
+        pairs = kvform.kvToSeq(assoc_s, strict=True)
+        keys = []
+        values = []
+        for k, v in pairs:
+            keys.append(k)
+            values.append(v)
+
+        if keys != cls.assoc_keys:
+            raise ValueError('Unexpected key values: %r', keys)
+
+        version, handle, secret, issued, lifetime, assoc_type = values
+        if version != '2':
+            raise ValueError('Unknown version: %r' % version)
+        issued = int(issued)
+        lifetime = int(lifetime)
+        secret = oidutil.fromBase64(secret)
+        return cls(handle, secret, issued, lifetime, assoc_type)
+
+    deserialize = classmethod(deserialize)
+
+    def sign(self, pairs):
+        """
+        Generate a signature for a sequence of (key, value) pairs
+
+
+        @param pairs: The pairs to sign, in order
+
+        @type pairs: sequence of (str, str)
+
+
+        @return: The binary signature of this sequence of pairs
+
+        @rtype: str
+        """
+        kv = kvform.seqToKV(pairs)
+
+        try:
+            mac = self._macs[self.assoc_type]
+        except KeyError:
+            raise ValueError(
+                'Unknown association type: %r' % (self.assoc_type,))
+
+        return mac(self.secret, kv)
+
+
+    def getMessageSignature(self, message):
+        """Return the signature of a message.
+
+        If I am not a sign-all association, the message must have a
+        signed list.
+
+        @return: the signature, base64 encoded
+
+        @rtype: str
+
+        @raises ValueError: If there is no signed list and I am not a sign-all
+            type of association.
+        """
+        pairs = self._makePairs(message)
+        return oidutil.toBase64(self.sign(pairs))
+
+    def signMessage(self, message):
+        """Add a signature (and a signed list) to a message.
+
+        @return: a new Message object with a signature
+        @rtype: L{openid.message.Message}
+        """
+        if (message.hasKey(OPENID_NS, 'sig') or
+            message.hasKey(OPENID_NS, 'signed')):
+            raise ValueError('Message already has signed list or signature')
+
+        extant_handle = message.getArg(OPENID_NS, 'assoc_handle')
+        if extant_handle and extant_handle != self.handle:
+            raise ValueError("Message has a different association handle")
+
+        signed_message = message.copy()
+        signed_message.setArg(OPENID_NS, 'assoc_handle', self.handle)
+        message_keys = signed_message.toPostArgs().keys()
+        signed_list = [k[7:] for k in message_keys
+                       if k.startswith('openid.')]
+        signed_list.append('signed')
+        signed_list.sort()
+        signed_message.setArg(OPENID_NS, 'signed', ','.join(signed_list))
+        sig = self.getMessageSignature(signed_message)
+        signed_message.setArg(OPENID_NS, 'sig', sig)
+        return signed_message
+
+    def checkMessageSignature(self, message):
+        """Given a message with a signature, calculate a new signature
+        and return whether it matches the signature in the message.
+
+        @raises ValueError: if the message has no signature or no signature
+            can be calculated for it.
+        """        
+        message_sig = message.getArg(OPENID_NS, 'sig')
+        if not message_sig:
+            raise ValueError("%s has no sig." % (message,))
+        calculated_sig = self.getMessageSignature(message)
+        return calculated_sig == message_sig
+
+
+    def _makePairs(self, message):
+        signed = message.getArg(OPENID_NS, 'signed')
+        if not signed:
+            raise ValueError('Message has no signed list: %s' % (message,))
+
+        signed_list = signed.split(',')
+        pairs = []
+        data = message.toPostArgs()
+        for field in signed_list:
+            pairs.append((field, data.get('openid.' + field, '')))
+        return pairs
+
+    def __repr__(self):
+        return "<%s.%s %s %s>" % (
+            self.__class__.__module__,
+            self.__class__.__name__,
+            self.assoc_type,
+            self.handle)

+ 6 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/consumer/__init__.py

@@ -0,0 +1,6 @@
+"""
+This package contains the portions of the library used only when
+implementing an OpenID consumer.
+"""
+
+__all__ = ['consumer', 'discover']

+ 1900 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/consumer/consumer.py

@@ -0,0 +1,1900 @@
+# -*- test-case-name: openid.test.test_consumer -*-
+"""OpenID support for Relying Parties (aka Consumers).
+
+This module documents the main interface with the OpenID consumer
+library.  The only part of the library which has to be used and isn't
+documented in full here is the store required to create an
+C{L{Consumer}} instance.  More on the abstract store type and
+concrete implementations of it that are provided in the documentation
+for the C{L{__init__<Consumer.__init__>}} method of the
+C{L{Consumer}} class.
+
+
+OVERVIEW
+========
+
+    The OpenID identity verification process most commonly uses the
+    following steps, as visible to the user of this library:
+
+        1. The user enters their OpenID into a field on the consumer's
+           site, and hits a login button.
+
+        2. The consumer site discovers the user's OpenID provider using
+           the Yadis protocol.
+
+        3. The consumer site sends the browser a redirect to the
+           OpenID provider.  This is the authentication request as
+           described in the OpenID specification.
+
+        4. The OpenID provider's site sends the browser a redirect
+           back to the consumer site.  This redirect contains the
+           provider's response to the authentication request.
+
+    The most important part of the flow to note is the consumer's site
+    must handle two separate HTTP requests in order to perform the
+    full identity check.
+
+
+LIBRARY DESIGN
+==============
+
+    This consumer library is designed with that flow in mind.  The
+    goal is to make it as easy as possible to perform the above steps
+    securely.
+
+    At a high level, there are two important parts in the consumer
+    library.  The first important part is this module, which contains
+    the interface to actually use this library.  The second is the
+    C{L{openid.store.interface}} module, which describes the
+    interface to use if you need to create a custom method for storing
+    the state this library needs to maintain between requests.
+
+    In general, the second part is less important for users of the
+    library to know about, as several implementations are provided
+    which cover a wide variety of situations in which consumers may
+    use the library.
+
+    This module contains a class, C{L{Consumer}}, with methods
+    corresponding to the actions necessary in each of steps 2, 3, and
+    4 described in the overview.  Use of this library should be as easy
+    as creating an C{L{Consumer}} instance and calling the methods
+    appropriate for the action the site wants to take.
+
+
+SESSIONS, STORES, AND STATELESS MODE
+====================================
+
+    The C{L{Consumer}} object keeps track of two types of state:
+
+        1. State of the user's current authentication attempt.  Things like
+           the identity URL, the list of endpoints discovered for that
+           URL, and in case where some endpoints are unreachable, the list
+           of endpoints already tried.  This state needs to be held from
+           Consumer.begin() to Consumer.complete(), but it is only applicable
+           to a single session with a single user agent, and at the end of
+           the authentication process (i.e. when an OP replies with either
+           C{id_res} or C{cancel}) it may be discarded.
+
+        2. State of relationships with servers, i.e. shared secrets
+           (associations) with servers and nonces seen on signed messages.
+           This information should persist from one session to the next and
+           should not be bound to a particular user-agent.
+
+
+    These two types of storage are reflected in the first two arguments of
+    Consumer's constructor, C{session} and C{store}.  C{session} is a
+    dict-like object and we hope your web framework provides you with one
+    of these bound to the user agent.  C{store} is an instance of
+    L{openid.store.interface.OpenIDStore}.
+
+    Since the store does hold secrets shared between your application and the
+    OpenID provider, you should be careful about how you use it in a shared
+    hosting environment.  If the filesystem or database permissions of your
+    web host allow strangers to read from them, do not store your data there!
+    If you have no safe place to store your data, construct your consumer
+    with C{None} for the store, and it will operate only in stateless mode.
+    Stateless mode may be slower, put more load on the OpenID provider, and
+    trusts the provider to keep you safe from replay attacks.
+
+
+    Several store implementation are provided, and the interface is
+    fully documented so that custom stores can be used as well.  See
+    the documentation for the C{L{Consumer}} class for more
+    information on the interface for stores.  The implementations that
+    are provided allow the consumer site to store the necessary data
+    in several different ways, including several SQL databases and
+    normal files on disk.
+
+
+IMMEDIATE MODE
+==============
+
+    In the flow described above, the user may need to confirm to the
+    OpenID provider that it's ok to disclose his or her identity.
+    The provider may draw pages asking for information from the user
+    before it redirects the browser back to the consumer's site.  This
+    is generally transparent to the consumer site, so it is typically
+    ignored as an implementation detail.
+
+    There can be times, however, where the consumer site wants to get
+    a response immediately.  When this is the case, the consumer can
+    put the library in immediate mode.  In immediate mode, there is an
+    extra response possible from the server, which is essentially the
+    server reporting that it doesn't have enough information to answer
+    the question yet.
+
+
+USING THIS LIBRARY
+==================
+
+    Integrating this library into an application is usually a
+    relatively straightforward process.  The process should basically
+    follow this plan:
+
+    Add an OpenID login field somewhere on your site.  When an OpenID
+    is entered in that field and the form is submitted, it should make
+    a request to your site which includes that OpenID URL.
+
+    First, the application should L{instantiate a Consumer<Consumer.__init__>}
+    with a session for per-user state and store for shared state.
+    using the store of choice.
+
+    Next, the application should call the 'C{L{begin<Consumer.begin>}}' method on the
+    C{L{Consumer}} instance.  This method takes the OpenID URL.  The
+    C{L{begin<Consumer.begin>}} method returns an C{L{AuthRequest}}
+    object.
+
+    Next, the application should call the
+    C{L{redirectURL<AuthRequest.redirectURL>}} method on the
+    C{L{AuthRequest}} object.  The parameter C{return_to} is the URL
+    that the OpenID server will send the user back to after attempting
+    to verify his or her identity.  The C{realm} parameter is the
+    URL (or URL pattern) that identifies your web site to the user
+    when he or she is authorizing it.  Send a redirect to the
+    resulting URL to the user's browser.
+
+    That's the first half of the authentication process.  The second
+    half of the process is done after the user's OpenID Provider sends the
+    user's browser a redirect back to your site to complete their
+    login.
+
+    When that happens, the user will contact your site at the URL
+    given as the C{return_to} URL to the
+    C{L{redirectURL<AuthRequest.redirectURL>}} call made
+    above.  The request will have several query parameters added to
+    the URL by the OpenID provider as the information necessary to
+    finish the request.
+
+    Get a C{L{Consumer}} instance with the same session and store as
+    before and call its C{L{complete<Consumer.complete>}} method,
+    passing in all the received query arguments.
+
+    There are multiple possible return types possible from that
+    method. These indicate whether or not the login was successful,
+    and include any additional information appropriate for their type.
+
+@var SUCCESS: constant used as the status for
+    L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects.
+
+@var FAILURE: constant used as the status for
+    L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects.
+
+@var CANCEL: constant used as the status for
+    L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects.
+
+@var SETUP_NEEDED: constant used as the status for
+    L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
+    objects.
+"""
+
+import cgi
+import copy
+from urlparse import urlparse, urldefrag
+
+from openid import fetchers
+
+from openid.consumer.discover import discover, OpenIDServiceEndpoint, \
+     DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE
+from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \
+     IDENTIFIER_SELECT, no_default, BARE_NS
+from openid import cryptutil
+from openid import oidutil
+from openid.association import Association, default_negotiator, \
+     SessionNegotiator
+from openid.dh import DiffieHellman
+from openid.store.nonce import mkNonce, split as splitNonce
+from openid.yadis.manager import Discovery
+from openid import urinorm
+
+
+__all__ = ['AuthRequest', 'Consumer', 'SuccessResponse',
+           'SetupNeededResponse', 'CancelResponse', 'FailureResponse',
+           'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED',
+           ]
+
+
+def makeKVPost(request_message, server_url):
+    """Make a Direct Request to an OpenID Provider and return the
+    result as a Message object.
+
+    @raises openid.fetchers.HTTPFetchingError: if an error is
+        encountered in making the HTTP post.
+
+    @rtype: L{openid.message.Message}
+    """
+    # XXX: TESTME
+    resp = fetchers.fetch(server_url, body=request_message.toURLEncoded())
+
+    # Process response in separate function that can be shared by async code.
+    return _httpResponseToMessage(resp, server_url)
+
+
+def _httpResponseToMessage(response, server_url):
+    """Adapt a POST response to a Message.
+
+    @type response: L{openid.fetchers.HTTPResponse}
+    @param response: Result of a POST to an OpenID endpoint.
+
+    @rtype: L{openid.message.Message}
+
+    @raises openid.fetchers.HTTPFetchingError: if the server returned a
+        status of other than 200 or 400.
+
+    @raises ServerError: if the server returned an OpenID error.
+    """
+    # Should this function be named Message.fromHTTPResponse instead?
+    response_message = Message.fromKVForm(response.body)
+    if response.status == 400:
+        raise ServerError.fromMessage(response_message)
+
+    elif response.status not in (200, 206):
+        fmt = 'bad status code from server %s: %s'
+        error_message = fmt % (server_url, response.status)
+        raise fetchers.HTTPFetchingError(error_message)
+
+    return response_message
+
+
+
+class Consumer(object):
+    """An OpenID consumer implementation that performs discovery and
+    does session management.
+
+    @ivar consumer: an instance of an object implementing the OpenID
+        protocol, but doing no discovery or session management.
+
+    @type consumer: GenericConsumer
+
+    @ivar session: A dictionary-like object representing the user's
+        session data.  This is used for keeping state of the OpenID
+        transaction when the user is redirected to the server.
+
+    @cvar session_key_prefix: A string that is prepended to session
+        keys to ensure that they are unique. This variable may be
+        changed to suit your application.
+    """
+    session_key_prefix = "_openid_consumer_"
+
+    _token = 'last_token'
+
+    _discover = staticmethod(discover)
+
+    def __init__(self, session, store, consumer_class=None):
+        """Initialize a Consumer instance.
+
+        You should create a new instance of the Consumer object with
+        every HTTP request that handles OpenID transactions.
+
+        @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>}
+
+        @param store: an object that implements the interface in
+            C{L{openid.store.interface.OpenIDStore}}.  Several
+            implementations are provided, to cover common database
+            environments.
+
+        @type store: C{L{openid.store.interface.OpenIDStore}}
+
+        @see: L{openid.store.interface}
+        @see: L{openid.store}
+        """
+        self.session = session
+        if consumer_class is None:
+            consumer_class = GenericConsumer
+        self.consumer = consumer_class(store)
+        self._token_key = self.session_key_prefix + self._token
+
+    def begin(self, user_url, anonymous=False):
+        """Start the OpenID authentication process. See steps 1-2 in
+        the overview at the top of this file.
+
+        @param user_url: Identity URL given by the user. This method
+            performs a textual transformation of the URL to try and
+            make sure it is normalized. For example, a user_url of
+            example.com will be normalized to http://example.com/
+            normalizing and resolving any redirects the server might
+            issue.
+
+        @type user_url: unicode
+
+        @param anonymous: Whether to make an anonymous request of the OpenID
+            provider.  Such a request does not ask for an authorization
+            assertion for an OpenID identifier, but may be used with
+            extensions to pass other data.  e.g. "I don't care who you are,
+            but I'd like to know your time zone."
+
+        @type anonymous: bool
+
+        @returns: An object containing the discovered information will
+            be returned, with a method for building a redirect URL to
+            the server, as described in step 3 of the overview. This
+            object may also be used to add extension arguments to the
+            request, using its
+            L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>}
+            method.
+
+        @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
+
+        @raises openid.consumer.discover.DiscoveryFailure: when I fail to
+            find an OpenID server for this URL.  If the C{yadis} package
+            is available, L{openid.consumer.discover.DiscoveryFailure} is
+            an alias for C{yadis.discover.DiscoveryFailure}.
+        """
+        disco = Discovery(self.session, user_url, self.session_key_prefix)
+        try:
+            service = disco.getNextService(self._discover)
+        except fetchers.HTTPFetchingError, why:
+            raise DiscoveryFailure(
+                'Error fetching XRDS document: %s' % (why[0],), None)
+
+        if service is None:
+            raise DiscoveryFailure(
+                'No usable OpenID services found for %s' % (user_url,), None)
+        else:
+            return self.beginWithoutDiscovery(service, anonymous)
+
+    def beginWithoutDiscovery(self, service, anonymous=False):
+        """Start OpenID verification without doing OpenID server
+        discovery. This method is used internally by Consumer.begin
+        after discovery is performed, and exists to provide an
+        interface for library users needing to perform their own
+        discovery.
+
+        @param service: an OpenID service endpoint descriptor.  This
+            object and factories for it are found in the
+            L{openid.consumer.discover} module.
+
+        @type service:
+            L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
+
+        @returns: an OpenID authentication request object.
+
+        @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
+
+        @See: Openid.consumer.consumer.Consumer.begin
+        @see: openid.consumer.discover
+        """
+        auth_req = self.consumer.begin(service)
+        self.session[self._token_key] = auth_req.endpoint
+
+        try:
+            auth_req.setAnonymous(anonymous)
+        except ValueError, why:
+            raise ProtocolError(str(why))
+
+        return auth_req
+
+    def complete(self, query, current_url):
+        """Called to interpret the server's response to an OpenID
+        request. It is called in step 4 of the flow described in the
+        consumer overview.
+
+        @param query: A dictionary of the query parameters for this
+            HTTP request.
+
+        @param current_url: The URL used to invoke the application.
+            Extract the URL from your application's web
+            request framework and specify it here to have it checked
+            against the openid.return_to value in the response.  If
+            the return_to URL check fails, the status of the
+            completion will be FAILURE.
+
+        @returns: a subclass of Response. The type of response is
+            indicated by the status attribute, which will be one of
+            SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
+
+        @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>}
+        @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>}
+        @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
+        @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>}
+        """
+
+        endpoint = self.session.get(self._token_key)
+
+        message = Message.fromPostArgs(query)
+        response = self.consumer.complete(message, endpoint, current_url)
+
+        try:
+            del self.session[self._token_key]
+        except KeyError:
+            pass
+
+        if (response.status in ['success', 'cancel'] and
+            response.identity_url is not None):
+
+            disco = Discovery(self.session,
+                              response.identity_url,
+                              self.session_key_prefix)
+            # This is OK to do even if we did not do discovery in
+            # the first place.
+            disco.cleanup(force=True)
+
+        return response
+
+    def setAssociationPreference(self, association_preferences):
+        """Set the order in which association types/sessions should be
+        attempted. For instance, to only allow HMAC-SHA256
+        associations created with a DH-SHA256 association session:
+
+        >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
+
+        Any association type/association type pair that is not in this
+        list will not be attempted at all.
+
+        @param association_preferences: The list of allowed
+            (association type, association session type) pairs that
+            should be allowed for this consumer to use, in order from
+            most preferred to least preferred.
+        @type association_preferences: [(str, str)]
+
+        @returns: None
+
+        @see: C{L{openid.association.SessionNegotiator}}
+        """
+        self.consumer.negotiator = SessionNegotiator(association_preferences)
+
+class DiffieHellmanSHA1ConsumerSession(object):
+    session_type = 'DH-SHA1'
+    hash_func = staticmethod(cryptutil.sha1)
+    secret_size = 20
+    allowed_assoc_types = ['HMAC-SHA1']
+
+    def __init__(self, dh=None):
+        if dh is None:
+            dh = DiffieHellman.fromDefaults()
+
+        self.dh = dh
+
+    def getRequest(self):
+        cpub = cryptutil.longToBase64(self.dh.public)
+
+        args = {'dh_consumer_public': cpub}
+
+        if not self.dh.usingDefaultValues():
+            args.update({
+                'dh_modulus': cryptutil.longToBase64(self.dh.modulus),
+                'dh_gen': cryptutil.longToBase64(self.dh.generator),
+                })
+
+        return args
+
+    def extractSecret(self, response):
+        dh_server_public64 = response.getArg(
+            OPENID_NS, 'dh_server_public', no_default)
+        enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default)
+        dh_server_public = cryptutil.base64ToLong(dh_server_public64)
+        enc_mac_key = oidutil.fromBase64(enc_mac_key64)
+        return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
+
+class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
+    session_type = 'DH-SHA256'
+    hash_func = staticmethod(cryptutil.sha256)
+    secret_size = 32
+    allowed_assoc_types = ['HMAC-SHA256']
+
+class PlainTextConsumerSession(object):
+    session_type = 'no-encryption'
+    allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
+
+    def getRequest(self):
+        return {}
+
+    def extractSecret(self, response):
+        mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
+        return oidutil.fromBase64(mac_key64)
+
+class SetupNeededError(Exception):
+    """Internally-used exception that indicates that an immediate-mode
+    request cancelled."""
+    def __init__(self, user_setup_url=None):
+        Exception.__init__(self, user_setup_url)
+        self.user_setup_url = user_setup_url
+
+class ProtocolError(ValueError):
+    """Exception that indicates that a message violated the
+    protocol. It is raised and caught internally to this file."""
+
+class TypeURIMismatch(ProtocolError):
+    """A protocol error arising from type URIs mismatching
+    """
+
+    def __init__(self, expected, endpoint):
+        ProtocolError.__init__(self, expected, endpoint)
+        self.expected = expected
+        self.endpoint = endpoint
+
+    def __str__(self):
+        s = '<%s.%s: Required type %s not found in %s for endpoint %s>' % (
+            self.__class__.__module__, self.__class__.__name__,
+            self.expected, self.endpoint.type_uris, self.endpoint)
+        return s
+
+
+
+class ServerError(Exception):
+    """Exception that is raised when the server returns a 400 response
+    code to a direct request."""
+
+    def __init__(self, error_text, error_code, message):
+        Exception.__init__(self, error_text)
+        self.error_text = error_text
+        self.error_code = error_code
+        self.message = message
+
+    def fromMessage(cls, message):
+        """Generate a ServerError instance, extracting the error text
+        and the error code from the message."""
+        error_text = message.getArg(
+            OPENID_NS, 'error', '<no error message supplied>')
+        error_code = message.getArg(OPENID_NS, 'error_code')
+        return cls(error_text, error_code, message)
+
+    fromMessage = classmethod(fromMessage)
+
+class GenericConsumer(object):
+    """This is the implementation of the common logic for OpenID
+    consumers. It is unaware of the application in which it is
+    running.
+
+    @ivar negotiator: An object that controls the kind of associations
+        that the consumer makes. It defaults to
+        C{L{openid.association.default_negotiator}}. Assign a
+        different negotiator to it if you have specific requirements
+        for how associations are made.
+    @type negotiator: C{L{openid.association.SessionNegotiator}}
+    """
+
+    # The name of the query parameter that gets added to the return_to
+    # URL when using OpenID1. You can change this value if you want or
+    # need a different name, but don't make it start with openid,
+    # because it's not a standard protocol thing for OpenID1. For
+    # OpenID2, the library will take care of the nonce using standard
+    # OpenID query parameter names.
+    openid1_nonce_query_arg_name = 'janrain_nonce'
+
+    # Another query parameter that gets added to the return_to for
+    # OpenID 1; if the user's session state is lost, use this claimed
+    # identifier to do discovery when verifying the response.
+    openid1_return_to_identifier_name = 'openid1_claimed_id'
+
+    session_types = {
+        'DH-SHA1':DiffieHellmanSHA1ConsumerSession,
+        'DH-SHA256':DiffieHellmanSHA256ConsumerSession,
+        'no-encryption':PlainTextConsumerSession,
+        }
+
+    _discover = staticmethod(discover)
+
+    def __init__(self, store):
+        self.store = store
+        self.negotiator = default_negotiator.copy()
+
+    def begin(self, service_endpoint):
+        """Create an AuthRequest object for the specified
+        service_endpoint. This method will create an association if
+        necessary."""
+        if self.store is None:
+            assoc = None
+        else:
+            assoc = self._getAssociation(service_endpoint)
+
+        request = AuthRequest(service_endpoint, assoc)
+        request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
+
+        if request.message.isOpenID1():
+            request.return_to_args[self.openid1_return_to_identifier_name] = \
+                request.endpoint.claimed_id
+
+        return request
+
+    def complete(self, message, endpoint, return_to):
+        """Process the OpenID message, using the specified endpoint
+        and return_to URL as context. This method will handle any
+        OpenID message that is sent to the return_to URL.
+        """
+        mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
+
+        modeMethod = getattr(self, '_complete_' + mode,
+                             self._completeInvalid)
+
+        return modeMethod(message, endpoint, return_to)
+
+    def _complete_cancel(self, message, endpoint, _):
+        return CancelResponse(endpoint)
+
+    def _complete_error(self, message, endpoint, _):
+        error = message.getArg(OPENID_NS, 'error')
+        contact = message.getArg(OPENID_NS, 'contact')
+        reference = message.getArg(OPENID_NS, 'reference')
+
+        return FailureResponse(endpoint, error, contact=contact,
+                               reference=reference)
+
+    def _complete_setup_needed(self, message, endpoint, _):
+        if not message.isOpenID2():
+            return self._completeInvalid(message, endpoint, _)
+
+        user_setup_url = message.getArg(OPENID2_NS, 'user_setup_url')
+        return SetupNeededResponse(endpoint, user_setup_url)
+
+    def _complete_id_res(self, message, endpoint, return_to):
+        try:
+            self._checkSetupNeeded(message)
+        except SetupNeededError, why:
+            return SetupNeededResponse(endpoint, why.user_setup_url)
+        else:
+            try:
+                return self._doIdRes(message, endpoint, return_to)
+            except (ProtocolError, DiscoveryFailure), why:
+                return FailureResponse(endpoint, why[0])
+
+    def _completeInvalid(self, message, endpoint, _):
+        mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
+        return FailureResponse(endpoint,
+                               'Invalid openid.mode: %r' % (mode,))
+
+    def _checkReturnTo(self, message, return_to):
+        """Check an OpenID message and its openid.return_to value
+        against a return_to URL from an application.  Return True on
+        success, False on failure.
+        """
+        # Check the openid.return_to args against args in the original
+        # message.
+        try:
+            self._verifyReturnToArgs(message.toPostArgs())
+        except ProtocolError, why:
+            oidutil.log("Verifying return_to arguments: %s" % (why[0],))
+            return False
+
+        # Check the return_to base URL against the one in the message.
+        msg_return_to = message.getArg(OPENID_NS, 'return_to')
+
+        # The URL scheme, authority, and path MUST be the same between
+        # the two URLs.
+        app_parts = urlparse(urinorm.urinorm(return_to))
+        msg_parts = urlparse(urinorm.urinorm(msg_return_to))
+
+        # (addressing scheme, network location, path) must be equal in
+        # both URLs.
+        for part in range(0, 3):
+            if app_parts[part] != msg_parts[part]:
+                return False
+
+        return True
+
+    _makeKVPost = staticmethod(makeKVPost)
+
+    def _checkSetupNeeded(self, message):
+        """Check an id_res message to see if it is a
+        checkid_immediate cancel response.
+
+        @raises SetupNeededError: if it is a checkid_immediate cancellation
+        """
+        # In OpenID 1, we check to see if this is a cancel from
+        # immediate mode by the presence of the user_setup_url
+        # parameter.
+        if message.isOpenID1():
+            user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
+            if user_setup_url is not None:
+                raise SetupNeededError(user_setup_url)
+
+    def _doIdRes(self, message, endpoint, return_to):
+        """Handle id_res responses that are not cancellations of
+        immediate mode requests.
+
+        @param message: the response paramaters.
+        @param endpoint: the discovered endpoint object. May be None.
+
+        @raises ProtocolError: If the message contents are not
+            well-formed according to the OpenID specification. This
+            includes missing fields or not signing fields that should
+            be signed.
+
+        @raises DiscoveryFailure: If the subject of the id_res message
+            does not match the supplied endpoint, and discovery on the
+            identifier in the message fails (this should only happen
+            when using OpenID 2)
+
+        @returntype: L{Response}
+        """
+        # Checks for presence of appropriate fields (and checks
+        # signed list fields)
+        self._idResCheckForFields(message)
+
+        if not self._checkReturnTo(message, return_to):
+            raise ProtocolError(
+                "return_to does not match return URL. Expected %r, got %r"
+                % (return_to, message.getArg(OPENID_NS, 'return_to')))
+
+
+        # Verify discovery information:
+        endpoint = self._verifyDiscoveryResults(message, endpoint)
+        oidutil.log("Received id_res response from %s using association %s" %
+                    (endpoint.server_url,
+                     message.getArg(OPENID_NS, 'assoc_handle')))
+
+        self._idResCheckSignature(message, endpoint.server_url)
+
+        # Will raise a ProtocolError if the nonce is bad
+        self._idResCheckNonce(message, endpoint)
+
+        signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
+        signed_list = signed_list_str.split(',')
+        signed_fields = ["openid." + s for s in signed_list]
+        return SuccessResponse(endpoint, message, signed_fields)
+
+    def _idResGetNonceOpenID1(self, message, endpoint):
+        """Extract the nonce from an OpenID 1 response.  Return the
+        nonce from the BARE_NS since we independently check the
+        return_to arguments are the same as those in the response
+        message.
+
+        See the openid1_nonce_query_arg_name class variable
+
+        @returns: The nonce as a string or None
+        """
+        return message.getArg(BARE_NS, self.openid1_nonce_query_arg_name)
+
+    def _idResCheckNonce(self, message, endpoint):
+        if message.isOpenID1():
+            # This indicates that the nonce was generated by the consumer
+            nonce = self._idResGetNonceOpenID1(message, endpoint)
+            server_url = ''
+        else:
+            nonce = message.getArg(OPENID2_NS, 'response_nonce')
+            server_url = endpoint.server_url
+
+        if nonce is None:
+            raise ProtocolError('Nonce missing from response')
+
+        try:
+            timestamp, salt = splitNonce(nonce)
+        except ValueError, why:
+            raise ProtocolError('Malformed nonce: %s' % (why[0],))
+
+        if (self.store is not None and
+            not self.store.useNonce(server_url, timestamp, salt)):
+            raise ProtocolError('Nonce already used or out of range')
+
+    def _idResCheckSignature(self, message, server_url):
+        assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
+        if self.store is None:
+            assoc = None
+        else:
+            assoc = self.store.getAssociation(server_url, assoc_handle)
+
+        if assoc:
+            if assoc.getExpiresIn() <= 0:
+                # XXX: It might be a good idea sometimes to re-start the
+                # authentication with a new association. Doing it
+                # automatically opens the possibility for
+                # denial-of-service by a server that just returns expired
+                # associations (or really short-lived associations)
+                raise ProtocolError(
+                    'Association with %s expired' % (server_url,))
+
+            if not assoc.checkMessageSignature(message):
+                raise ProtocolError('Bad signature')
+
+        else:
+            # It's not an association we know about.  Stateless mode is our
+            # only possible path for recovery.
+            # XXX - async framework will not want to block on this call to
+            # _checkAuth.
+            if not self._checkAuth(message, server_url):
+                raise ProtocolError('Server denied check_authentication')
+
+    def _idResCheckForFields(self, message):
+        # XXX: this should be handled by the code that processes the
+        # response (that is, if a field is missing, we should not have
+        # to explicitly check that it's present, just make sure that
+        # the fields are actually being used by the rest of the code
+        # in tests). Although, which fields are signed does need to be
+        # checked somewhere.
+        basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
+        basic_sig_fields = ['return_to', 'identity']
+
+        require_fields = {
+            OPENID2_NS: basic_fields + ['op_endpoint'],
+            OPENID1_NS: basic_fields + ['identity'],
+            }
+
+        require_sigs = {
+            OPENID2_NS: basic_sig_fields + ['response_nonce',
+                                            'claimed_id',
+                                            'assoc_handle',
+                                            'op_endpoint',],
+            OPENID1_NS: basic_sig_fields,
+            }
+
+        for field in require_fields[message.getOpenIDNamespace()]:
+            if not message.hasKey(OPENID_NS, field):
+                raise ProtocolError('Missing required field %r' % (field,))
+
+        signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
+        signed_list = signed_list_str.split(',')
+
+        for field in require_sigs[message.getOpenIDNamespace()]:
+            # Field is present and not in signed list
+            if message.hasKey(OPENID_NS, field) and field not in signed_list:
+                raise ProtocolError('"%s" not signed' % (field,))
+
+
+    def _verifyReturnToArgs(query):
+        """Verify that the arguments in the return_to URL are present in this
+        response.
+        """
+        message = Message.fromPostArgs(query)
+        return_to = message.getArg(OPENID_NS, 'return_to')
+
+        if return_to is None:
+            raise ProtocolError('Response has no return_to')
+
+        parsed_url = urlparse(return_to)
+        rt_query = parsed_url[4]
+        parsed_args = cgi.parse_qsl(rt_query)
+
+        for rt_key, rt_value in parsed_args:
+            try:
+                value = query[rt_key]
+                if rt_value != value:
+                    format = ("parameter %s value %r does not match "
+                              "return_to's value %r")
+                    raise ProtocolError(format % (rt_key, value, rt_value))
+            except KeyError:
+                format = "return_to parameter %s absent from query %r"
+                raise ProtocolError(format % (rt_key, query))
+
+        # Make sure all non-OpenID arguments in the response are also
+        # in the signed return_to.
+        bare_args = message.getArgs(BARE_NS)
+        for pair in bare_args.iteritems():
+            if pair not in parsed_args:
+                raise ProtocolError("Parameter %s not in return_to URL" % (pair[0],))
+
+    _verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
+
+    def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
+        """
+        Extract the information from an OpenID assertion message and
+        verify it against the original
+
+        @param endpoint: The endpoint that resulted from doing discovery
+        @param resp_msg: The id_res message object
+
+        @returns: the verified endpoint
+        """
+        if resp_msg.getOpenIDNamespace() == OPENID2_NS:
+            return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
+        else:
+            return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
+
+
+    def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
+        to_match = OpenIDServiceEndpoint()
+        to_match.type_uris = [OPENID_2_0_TYPE]
+        to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
+        to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
+
+        # Raises a KeyError when the op_endpoint is not present
+        to_match.server_url = resp_msg.getArg(
+            OPENID2_NS, 'op_endpoint', no_default)
+
+        # claimed_id and identifier must both be present or both
+        # be absent
+        if (to_match.claimed_id is None and
+            to_match.local_id is not None):
+            raise ProtocolError(
+                'openid.identity is present without openid.claimed_id')
+
+        elif (to_match.claimed_id is not None and
+              to_match.local_id is None):
+            raise ProtocolError(
+                'openid.claimed_id is present without openid.identity')
+
+        # This is a response without identifiers, so there's really no
+        # checking that we can do, so return an endpoint that's for
+        # the specified `openid.op_endpoint'
+        elif to_match.claimed_id is None:
+            return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
+
+        # The claimed ID doesn't match, so we have to do discovery
+        # again. This covers not using sessions, OP identifier
+        # endpoints and responses that didn't match the original
+        # request.
+        if not endpoint:
+            oidutil.log('No pre-discovered information supplied.')
+            endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match])
+        else:
+            # The claimed ID matches, so we use the endpoint that we
+            # discovered in initiation. This should be the most common
+            # case.
+            try:
+                self._verifyDiscoverySingle(endpoint, to_match)
+            except ProtocolError, e:
+                oidutil.log(
+                    "Error attempting to use stored discovery information: " +
+                    str(e))
+                oidutil.log("Attempting discovery to verify endpoint")
+                endpoint = self._discoverAndVerify(
+                    to_match.claimed_id, [to_match])
+
+        # The endpoint we return should have the claimed ID from the
+        # message we just verified, fragment and all.
+        if endpoint.claimed_id != to_match.claimed_id:
+            endpoint = copy.copy(endpoint)
+            endpoint.claimed_id = to_match.claimed_id
+        return endpoint
+
+    def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
+        claimed_id = resp_msg.getArg(BARE_NS, self.openid1_return_to_identifier_name)
+
+        if endpoint is None and claimed_id is None:
+            raise RuntimeError(
+                'When using OpenID 1, the claimed ID must be supplied, '
+                'either by passing it through as a return_to parameter '
+                'or by using a session, and supplied to the GenericConsumer '
+                'as the argument to complete()')
+        elif endpoint is not None and claimed_id is None:
+            claimed_id = endpoint.claimed_id
+
+        to_match = OpenIDServiceEndpoint()
+        to_match.type_uris = [OPENID_1_1_TYPE]
+        to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
+        # Restore delegate information from the initiation phase
+        to_match.claimed_id = claimed_id
+
+        if to_match.local_id is None:
+            raise ProtocolError('Missing required field openid.identity')
+
+        to_match_1_0 = copy.copy(to_match)
+        to_match_1_0.type_uris = [OPENID_1_0_TYPE]
+
+        if endpoint is not None:
+            try:
+                try:
+                    self._verifyDiscoverySingle(endpoint, to_match)
+                except TypeURIMismatch:
+                    self._verifyDiscoverySingle(endpoint, to_match_1_0)
+            except ProtocolError, e:
+                oidutil.log("Error attempting to use stored discovery information: " +
+                            str(e))
+                oidutil.log("Attempting discovery to verify endpoint")
+            else:
+                return endpoint
+
+        # Endpoint is either bad (failed verification) or None
+        return self._discoverAndVerify(claimed_id, [to_match, to_match_1_0])
+
+    def _verifyDiscoverySingle(self, endpoint, to_match):
+        """Verify that the given endpoint matches the information
+        extracted from the OpenID assertion, and raise an exception if
+        there is a mismatch.
+
+        @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
+        @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
+
+        @rtype: NoneType
+
+        @raises ProtocolError: when the endpoint does not match the
+            discovered information.
+        """
+        # Every type URI that's in the to_match endpoint has to be
+        # present in the discovered endpoint.
+        for type_uri in to_match.type_uris:
+            if not endpoint.usesExtension(type_uri):
+                raise TypeURIMismatch(type_uri, endpoint)
+
+        # Fragments do not influence discovery, so we can't compare a
+        # claimed identifier with a fragment to discovered information.
+        defragged_claimed_id, _ = urldefrag(to_match.claimed_id)
+        if defragged_claimed_id != endpoint.claimed_id:
+            raise ProtocolError(
+                'Claimed ID does not match (different subjects!), '
+                'Expected %s, got %s' %
+                (defragged_claimed_id, endpoint.claimed_id))
+
+        if to_match.getLocalID() != endpoint.getLocalID():
+            raise ProtocolError('local_id mismatch. Expected %s, got %s' %
+                                (to_match.getLocalID(), endpoint.getLocalID()))
+
+        # If the server URL is None, this must be an OpenID 1
+        # response, because op_endpoint is a required parameter in
+        # OpenID 2. In that case, we don't actually care what the
+        # discovered server_url is, because signature checking or
+        # check_auth should take care of that check for us.
+        if to_match.server_url is None:
+            assert to_match.preferredNamespace() == OPENID1_NS, (
+                """The code calling this must ensure that OpenID 2
+                responses have a non-none `openid.op_endpoint' and
+                that it is set as the `server_url' attribute of the
+                `to_match' endpoint.""")
+
+        elif to_match.server_url != endpoint.server_url:
+            raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
+                                (to_match.server_url, endpoint.server_url))
+
+    def _discoverAndVerify(self, claimed_id, to_match_endpoints):
+        """Given an endpoint object created from the information in an
+        OpenID response, perform discovery and verify the discovery
+        results, returning the matching endpoint that is the result of
+        doing that discovery.
+
+        @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
+        @param to_match: The endpoint whose information we're confirming
+
+        @rtype: openid.consumer.discover.OpenIDServiceEndpoint
+        @returns: The result of performing discovery on the claimed
+            identifier in `to_match'
+
+        @raises DiscoveryFailure: when discovery fails.
+        """
+        oidutil.log('Performing discovery on %s' % (claimed_id,))
+        _, services = self._discover(claimed_id)
+        if not services:
+            raise DiscoveryFailure('No OpenID information found at %s' %
+                                   (claimed_id,), None)
+        return self._verifyDiscoveredServices(claimed_id, services,
+                                              to_match_endpoints)
+
+
+    def _verifyDiscoveredServices(self, claimed_id, services, to_match_endpoints):
+        """See @L{_discoverAndVerify}"""
+
+        # Search the services resulting from discovery to find one
+        # that matches the information from the assertion
+        failure_messages = []
+        for endpoint in services:
+            for to_match_endpoint in to_match_endpoints:
+                try:
+                    self._verifyDiscoverySingle(
+                        endpoint, to_match_endpoint)
+                except ProtocolError, why:
+                    failure_messages.append(str(why))
+                else:
+                    # It matches, so discover verification has
+                    # succeeded. Return this endpoint.
+                    return endpoint
+        else:
+            oidutil.log('Discovery verification failure for %s' %
+                        (claimed_id,))
+            for failure_message in failure_messages:
+                oidutil.log(' * Endpoint mismatch: ' + failure_message)
+
+            raise DiscoveryFailure(
+                'No matching endpoint found after discovering %s'
+                % (claimed_id,), None)
+
+    def _checkAuth(self, message, server_url):
+        """Make a check_authentication request to verify this message.
+
+        @returns: True if the request is valid.
+        @rtype: bool
+        """
+        oidutil.log('Using OpenID check_authentication')
+        request = self._createCheckAuthRequest(message)
+        if request is None:
+            return False
+        try:
+            response = self._makeKVPost(request, server_url)
+        except (fetchers.HTTPFetchingError, ServerError), e:
+            oidutil.log('check_authentication failed: %s' % (e[0],))
+            return False
+        else:
+            return self._processCheckAuthResponse(response, server_url)
+
+    def _createCheckAuthRequest(self, message):
+        """Generate a check_authentication request message given an
+        id_res message.
+        """
+        signed = message.getArg(OPENID_NS, 'signed')
+        if signed:
+            for k in signed.split(','):
+                oidutil.log(k)
+                val = message.getAliasedArg(k)
+
+                # Signed value is missing
+                if val is None:
+                    oidutil.log('Missing signed field %r' % (k,))
+                    return None
+
+        check_auth_message = message.copy()
+        check_auth_message.setArg(OPENID_NS, 'mode', 'check_authentication')
+        return check_auth_message
+
+    def _processCheckAuthResponse(self, response, server_url):
+        """Process the response message from a check_authentication
+        request, invalidating associations if requested.
+        """
+        is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
+
+        invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
+        if invalidate_handle is not None:
+            oidutil.log(
+                'Received "invalidate_handle" from server %s' % (server_url,))
+            if self.store is None:
+                oidutil.log('Unexpectedly got invalidate_handle without '
+                            'a store!')
+            else:
+                self.store.removeAssociation(server_url, invalidate_handle)
+
+        if is_valid == 'true':
+            return True
+        else:
+            oidutil.log('Server responds that checkAuth call is not valid')
+            return False
+
+    def _getAssociation(self, endpoint):
+        """Get an association for the endpoint's server_url.
+
+        First try seeing if we have a good association in the
+        store. If we do not, then attempt to negotiate an association
+        with the server.
+
+        If we negotiate a good association, it will get stored.
+
+        @returns: A valid association for the endpoint's server_url or None
+        @rtype: openid.association.Association or NoneType
+        """
+        assoc = self.store.getAssociation(endpoint.server_url)
+
+        if assoc is None or assoc.expiresIn <= 0:
+            assoc = self._negotiateAssociation(endpoint)
+            if assoc is not None:
+                self.store.storeAssociation(endpoint.server_url, assoc)
+
+        return assoc
+
+    def _negotiateAssociation(self, endpoint):
+        """Make association requests to the server, attempting to
+        create a new association.
+
+        @returns: a new association object
+
+        @rtype: L{openid.association.Association}
+        """
+        # Get our preferred session/association type from the negotiatior.
+        assoc_type, session_type = self.negotiator.getAllowedType()
+
+        try:
+            assoc = self._requestAssociation(
+                endpoint, assoc_type, session_type)
+        except ServerError, why:
+            supportedTypes = self._extractSupportedAssociationType(why,
+                                                                   endpoint,
+                                                                   assoc_type)
+            if supportedTypes is not None:
+                assoc_type, session_type = supportedTypes
+                # Attempt to create an association from the assoc_type
+                # and session_type that the server told us it
+                # supported.
+                try:
+                    assoc = self._requestAssociation(
+                        endpoint, assoc_type, session_type)
+                except ServerError, why:
+                    # Do not keep trying, since it rejected the
+                    # association type that it told us to use.
+                    oidutil.log('Server %s refused its suggested association '
+                                'type: session_type=%s, assoc_type=%s'
+                                % (endpoint.server_url, session_type,
+                                   assoc_type))
+                    return None
+                else:
+                    return assoc
+        else:
+            return assoc
+
+    def _extractSupportedAssociationType(self, server_error, endpoint,
+                                         assoc_type):
+        """Handle ServerErrors resulting from association requests.
+
+        @returns: If server replied with an C{unsupported-type} error,
+            return a tuple of supported C{association_type}, C{session_type}.
+            Otherwise logs the error and returns None.
+        @rtype: tuple or None
+        """
+        # Any error message whose code is not 'unsupported-type'
+        # should be considered a total failure.
+        if server_error.error_code != 'unsupported-type' or \
+               server_error.message.isOpenID1():
+            oidutil.log(
+                'Server error when requesting an association from %r: %s'
+                % (endpoint.server_url, server_error.error_text))
+            return None
+
+        # The server didn't like the association/session type
+        # that we sent, and it sent us back a message that
+        # might tell us how to handle it.
+        oidutil.log(
+            'Unsupported association type %s: %s' % (assoc_type,
+                                                     server_error.error_text,))
+
+        # Extract the session_type and assoc_type from the
+        # error message
+        assoc_type = server_error.message.getArg(OPENID_NS, 'assoc_type')
+        session_type = server_error.message.getArg(OPENID_NS, 'session_type')
+
+        if assoc_type is None or session_type is None:
+            oidutil.log('Server responded with unsupported association '
+                        'session but did not supply a fallback.')
+            return None
+        elif not self.negotiator.isAllowed(assoc_type, session_type):
+            fmt = ('Server sent unsupported session/association type: '
+                   'session_type=%s, assoc_type=%s')
+            oidutil.log(fmt % (session_type, assoc_type))
+            return None
+        else:
+            return assoc_type, session_type
+
+
+    def _requestAssociation(self, endpoint, assoc_type, session_type):
+        """Make and process one association request to this endpoint's
+        OP endpoint URL.
+
+        @returns: An association object or None if the association
+            processing failed.
+
+        @raises ServerError: when the remote OpenID server returns an error.
+        """
+        assoc_session, args = self._createAssociateRequest(
+            endpoint, assoc_type, session_type)
+
+        try:
+            response = self._makeKVPost(args, endpoint.server_url)
+        except fetchers.HTTPFetchingError, why:
+            oidutil.log('openid.associate request failed: %s' % (why[0],))
+            return None
+
+        try:
+            assoc = self._extractAssociation(response, assoc_session)
+        except KeyError, why:
+            oidutil.log('Missing required parameter in response from %s: %s'
+                        % (endpoint.server_url, why[0]))
+            return None
+        except ProtocolError, why:
+            oidutil.log('Protocol error parsing response from %s: %s' % (
+                endpoint.server_url, why[0]))
+            return None
+        else:
+            return assoc
+
+    def _createAssociateRequest(self, endpoint, assoc_type, session_type):
+        """Create an association request for the given assoc_type and
+        session_type.
+
+        @param endpoint: The endpoint whose server_url will be
+            queried. The important bit about the endpoint is whether
+            it's in compatiblity mode (OpenID 1.1)
+
+        @param assoc_type: The association type that the request
+            should ask for.
+        @type assoc_type: str
+
+        @param session_type: The session type that should be used in
+            the association request. The session_type is used to
+            create an association session object, and that session
+            object is asked for any additional fields that it needs to
+            add to the request.
+        @type session_type: str
+
+        @returns: a pair of the association session object and the
+            request message that will be sent to the server.
+        @rtype: (association session type (depends on session_type),
+                 openid.message.Message)
+        """
+        session_type_class = self.session_types[session_type]
+        assoc_session = session_type_class()
+
+        args = {
+            'mode': 'associate',
+            'assoc_type': assoc_type,
+            }
+
+        if not endpoint.compatibilityMode():
+            args['ns'] = OPENID2_NS
+
+        # Leave out the session type if we're in compatibility mode
+        # *and* it's no-encryption.
+        if (not endpoint.compatibilityMode() or
+            assoc_session.session_type != 'no-encryption'):
+            args['session_type'] = assoc_session.session_type
+
+        args.update(assoc_session.getRequest())
+        message = Message.fromOpenIDArgs(args)
+        return assoc_session, message
+
+    def _getOpenID1SessionType(self, assoc_response):
+        """Given an association response message, extract the OpenID
+        1.X session type.
+
+        This function mostly takes care of the 'no-encryption' default
+        behavior in OpenID 1.
+
+        If the association type is plain-text, this function will
+        return 'no-encryption'
+
+        @returns: The association type for this message
+        @rtype: str
+
+        @raises KeyError: when the session_type field is absent.
+        """
+        # If it's an OpenID 1 message, allow session_type to default
+        # to None (which signifies "no-encryption")
+        session_type = assoc_response.getArg(OPENID1_NS, 'session_type')
+
+        # Handle the differences between no-encryption association
+        # respones in OpenID 1 and 2:
+
+        # no-encryption is not really a valid session type for
+        # OpenID 1, but we'll accept it anyway, while issuing a
+        # warning.
+        if session_type == 'no-encryption':
+            oidutil.log('WARNING: OpenID server sent "no-encryption"'
+                        'for OpenID 1.X')
+
+        # Missing or empty session type is the way to flag a
+        # 'no-encryption' response. Change the session type to
+        # 'no-encryption' so that it can be handled in the same
+        # way as OpenID 2 'no-encryption' respones.
+        elif session_type == '' or session_type is None:
+            session_type = 'no-encryption'
+
+        return session_type
+
+    def _extractAssociation(self, assoc_response, assoc_session):
+        """Attempt to extract an association from the response, given
+        the association response message and the established
+        association session.
+
+        @param assoc_response: The association response message from
+            the server
+        @type assoc_response: openid.message.Message
+
+        @param assoc_session: The association session object that was
+            used when making the request
+        @type assoc_session: depends on the session type of the request
+
+        @raises ProtocolError: when data is malformed
+        @raises KeyError: when a field is missing
+
+        @rtype: openid.association.Association
+        """
+        # Extract the common fields from the response, raising an
+        # exception if they are not found
+        assoc_type = assoc_response.getArg(
+            OPENID_NS, 'assoc_type', no_default)
+        assoc_handle = assoc_response.getArg(
+            OPENID_NS, 'assoc_handle', no_default)
+
+        # expires_in is a base-10 string. The Python parsing will
+        # accept literals that have whitespace around them and will
+        # accept negative values. Neither of these are really in-spec,
+        # but we think it's OK to accept them.
+        expires_in_str = assoc_response.getArg(
+            OPENID_NS, 'expires_in', no_default)
+        try:
+            expires_in = int(expires_in_str)
+        except ValueError, why:
+            raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
+
+        # OpenID 1 has funny association session behaviour.
+        if assoc_response.isOpenID1():
+            session_type = self._getOpenID1SessionType(assoc_response)
+        else:
+            session_type = assoc_response.getArg(
+                OPENID2_NS, 'session_type', no_default)
+
+        # Session type mismatch
+        if assoc_session.session_type != session_type:
+            if (assoc_response.isOpenID1() and
+                session_type == 'no-encryption'):
+                # In OpenID 1, any association request can result in a
+                # 'no-encryption' association response. Setting
+                # assoc_session to a new no-encryption session should
+                # make the rest of this function work properly for
+                # that case.
+                assoc_session = PlainTextConsumerSession()
+            else:
+                # Any other mismatch, regardless of protocol version
+                # results in the failure of the association session
+                # altogether.
+                fmt = 'Session type mismatch. Expected %r, got %r'
+                message = fmt % (assoc_session.session_type, session_type)
+                raise ProtocolError(message)
+
+        # Make sure assoc_type is valid for session_type
+        if assoc_type not in assoc_session.allowed_assoc_types:
+            fmt = 'Unsupported assoc_type for session %s returned: %s'
+            raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
+
+        # Delegate to the association session to extract the secret
+        # from the response, however is appropriate for that session
+        # type.
+        try:
+            secret = assoc_session.extractSecret(assoc_response)
+        except ValueError, why:
+            fmt = 'Malformed response for %s session: %s'
+            raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
+
+        return Association.fromExpiresIn(
+            expires_in, assoc_handle, secret, assoc_type)
+
+class AuthRequest(object):
+    """An object that holds the state necessary for generating an
+    OpenID authentication request. This object holds the association
+    with the server and the discovered information with which the
+    request will be made.
+
+    It is separate from the consumer because you may wish to add
+    things to the request before sending it on its way to the
+    server. It also has serialization options that let you encode the
+    authentication request as a URL or as a form POST.
+    """
+
+    def __init__(self, endpoint, assoc):
+        """
+        Creates a new AuthRequest object.  This just stores each
+        argument in an appropriately named field.
+
+        Users of this library should not create instances of this
+        class.  Instances of this class are created by the library
+        when needed.
+        """
+        self.assoc = assoc
+        self.endpoint = endpoint
+        self.return_to_args = {}
+        self.message = Message(endpoint.preferredNamespace())
+        self._anonymous = False
+
+    def setAnonymous(self, is_anonymous):
+        """Set whether this request should be made anonymously. If a
+        request is anonymous, the identifier will not be sent in the
+        request. This is only useful if you are making another kind of
+        request with an extension in this request.
+
+        Anonymous requests are not allowed when the request is made
+        with OpenID 1.
+
+        @raises ValueError: when attempting to set an OpenID1 request
+            as anonymous
+        """
+        if is_anonymous and self.message.isOpenID1():
+            raise ValueError('OpenID 1 requests MUST include the '
+                             'identifier in the request')
+        else:
+            self._anonymous = is_anonymous
+
+    def addExtension(self, extension_request):
+        """Add an extension to this checkid request.
+
+        @param extension_request: An object that implements the
+            extension interface for adding arguments to an OpenID
+            message.
+        """
+        extension_request.toMessage(self.message)
+
+    def addExtensionArg(self, namespace, key, value):
+        """Add an extension argument to this OpenID authentication
+        request.
+
+        Use caution when adding arguments, because they will be
+        URL-escaped and appended to the redirect URL, which can easily
+        get quite long.
+
+        @param namespace: The namespace for the extension. For
+            example, the simple registration extension uses the
+            namespace C{sreg}.
+
+        @type namespace: str
+
+        @param key: The key within the extension namespace. For
+            example, the nickname field in the simple registration
+            extension's key is C{nickname}.
+
+        @type key: str
+
+        @param value: The value to provide to the server for this
+            argument.
+
+        @type value: str
+        """
+        self.message.setArg(namespace, key, value)
+
+    def getMessage(self, realm, return_to=None, immediate=False):
+        """Produce a L{openid.message.Message} representing this request.
+
+        @param realm: The URL (or URL pattern) that identifies your
+            web site to the user when she is authorizing it.
+
+        @type realm: str
+
+        @param return_to: The URL that the OpenID provider will send the
+            user back to after attempting to verify her identity.
+
+            Not specifying a return_to URL means that the user will not
+            be returned to the site issuing the request upon its
+            completion.
+
+        @type return_to: str
+
+        @param immediate: If True, the OpenID provider is to send back
+            a response immediately, useful for behind-the-scenes
+            authentication attempts.  Otherwise the OpenID provider
+            may engage the user before providing a response.  This is
+            the default case, as the user may need to provide
+            credentials or approve the request before a positive
+            response can be sent.
+
+        @type immediate: bool
+
+        @returntype: L{openid.message.Message}
+        """
+        if return_to:
+            return_to = oidutil.appendArgs(return_to, self.return_to_args)
+        elif immediate:
+            raise ValueError(
+                '"return_to" is mandatory when using "checkid_immediate"')
+        elif self.message.isOpenID1():
+            raise ValueError('"return_to" is mandatory for OpenID 1 requests')
+        elif self.return_to_args:
+            raise ValueError('extra "return_to" arguments were specified, '
+                             'but no return_to was specified')
+
+        if immediate:
+            mode = 'checkid_immediate'
+        else:
+            mode = 'checkid_setup'
+
+        message = self.message.copy()
+        if message.isOpenID1():
+            realm_key = 'trust_root'
+        else:
+            realm_key = 'realm'
+
+        message.updateArgs(OPENID_NS,
+            {
+            realm_key:realm,
+            'mode':mode,
+            'return_to':return_to,
+            })
+
+        if not self._anonymous:
+            if self.endpoint.isOPIdentifier():
+                # This will never happen when we're in compatibility
+                # mode, as long as isOPIdentifier() returns False
+                # whenever preferredNamespace() returns OPENID1_NS.
+                claimed_id = request_identity = IDENTIFIER_SELECT
+            else:
+                request_identity = self.endpoint.getLocalID()
+                claimed_id = self.endpoint.claimed_id
+
+            # This is true for both OpenID 1 and 2
+            message.setArg(OPENID_NS, 'identity', request_identity)
+
+            if message.isOpenID2():
+                message.setArg(OPENID2_NS, 'claimed_id', claimed_id)
+
+        if self.assoc:
+            message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle)
+            assoc_log_msg = 'with assocication %s' % (self.assoc.handle,)
+        else:
+            assoc_log_msg = 'using stateless mode.'
+
+        oidutil.log("Generated %s request to %s %s" %
+                    (mode, self.endpoint.server_url, assoc_log_msg))
+
+        return message
+
+    def redirectURL(self, realm, return_to=None, immediate=False):
+        """Returns a URL with an encoded OpenID request.
+
+        The resulting URL is the OpenID provider's endpoint URL with
+        parameters appended as query arguments.  You should redirect
+        the user agent to this URL.
+
+        OpenID 2.0 endpoints also accept POST requests, see
+        C{L{shouldSendRedirect}} and C{L{formMarkup}}.
+
+        @param realm: The URL (or URL pattern) that identifies your
+            web site to the user when she is authorizing it.
+
+        @type realm: str
+
+        @param return_to: The URL that the OpenID provider will send the
+            user back to after attempting to verify her identity.
+
+            Not specifying a return_to URL means that the user will not
+            be returned to the site issuing the request upon its
+            completion.
+
+        @type return_to: str
+
+        @param immediate: If True, the OpenID provider is to send back
+            a response immediately, useful for behind-the-scenes
+            authentication attempts.  Otherwise the OpenID provider
+            may engage the user before providing a response.  This is
+            the default case, as the user may need to provide
+            credentials or approve the request before a positive
+            response can be sent.
+
+        @type immediate: bool
+
+        @returns: The URL to redirect the user agent to.
+
+        @returntype: str
+        """
+        message = self.getMessage(realm, return_to, immediate)
+        return message.toURL(self.endpoint.server_url)
+
+    def formMarkup(self, realm, return_to=None, immediate=False,
+            form_tag_attrs=None):
+        """Get html for a form to submit this request to the IDP.
+
+        @param form_tag_attrs: Dictionary of attributes to be added to
+            the form tag. 'accept-charset' and 'enctype' have defaults
+            that can be overridden. If a value is supplied for
+            'action' or 'method', it will be replaced.
+        @type form_tag_attrs: {unicode: unicode}
+        """
+        message = self.getMessage(realm, return_to, immediate)
+        return message.toFormMarkup(self.endpoint.server_url,
+                    form_tag_attrs)
+
+    def htmlMarkup(self, realm, return_to=None, immediate=False,
+            form_tag_attrs=None):
+        """Get an autosubmitting HTML page that submits this request to the
+        IDP.  This is just a wrapper for formMarkup.
+
+        @see: formMarkup
+
+        @returns: str
+        """
+        return oidutil.autoSubmitHTML(self.formMarkup(realm, 
+                                                      return_to,
+                                                      immediate, 
+                                                      form_tag_attrs))
+
+    def shouldSendRedirect(self):
+        """Should this OpenID authentication request be sent as a HTTP
+        redirect or as a POST (form submission)?
+
+        @rtype: bool
+        """
+        return self.endpoint.compatibilityMode()
+
+FAILURE = 'failure'
+SUCCESS = 'success'
+CANCEL = 'cancel'
+SETUP_NEEDED = 'setup_needed'
+
+class Response(object):
+    status = None
+
+    def setEndpoint(self, endpoint):
+        self.endpoint = endpoint
+        if endpoint is None:
+            self.identity_url = None
+        else:
+            self.identity_url = endpoint.claimed_id
+
+    def getDisplayIdentifier(self):
+        """Return the display identifier for this response.
+
+        The display identifier is related to the Claimed Identifier, but the
+        two are not always identical.  The display identifier is something the
+        user should recognize as what they entered, whereas the response's
+        claimed identifier (in the L{identity_url} attribute) may have extra
+        information for better persistence.
+
+        URLs will be stripped of their fragments for display.  XRIs will
+        display the human-readable identifier (i-name) instead of the
+        persistent identifier (i-number).
+
+        Use the display identifier in your user interface.  Use
+        L{identity_url} for querying your database or authorization server.
+        """
+        if self.endpoint is not None:
+            return self.endpoint.getDisplayIdentifier()
+        return None
+
+class SuccessResponse(Response):
+    """A response with a status of SUCCESS. Indicates that this request is a
+    successful acknowledgement from the OpenID server that the
+    supplied URL is, indeed controlled by the requesting agent.
+
+    @ivar identity_url: The identity URL that has been authenticated; the Claimed Identifier.
+        See also L{getDisplayIdentifier}.
+
+    @ivar endpoint: The endpoint that authenticated the identifier.  You
+        may access other discovered information related to this endpoint,
+        such as the CanonicalID of an XRI, through this object.
+    @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
+
+    @ivar signed_fields: The arguments in the server's response that
+        were signed and verified.
+
+    @cvar status: SUCCESS
+    """
+
+    status = SUCCESS
+
+    def __init__(self, endpoint, message, signed_fields=None):
+        # Don't use setEndpoint, because endpoint should never be None
+        # for a successfull transaction.
+        self.endpoint = endpoint
+        self.identity_url = endpoint.claimed_id
+
+        self.message = message
+
+        if signed_fields is None:
+            signed_fields = []
+        self.signed_fields = signed_fields
+
+    def isOpenID1(self):
+        """Was this authentication response an OpenID 1 authentication
+        response?
+        """
+        return self.message.isOpenID1()
+
+    def isSigned(self, ns_uri, ns_key):
+        """Return whether a particular key is signed, regardless of
+        its namespace alias
+        """
+        return self.message.getKey(ns_uri, ns_key) in self.signed_fields
+
+    def getSigned(self, ns_uri, ns_key, default=None):
+        """Return the specified signed field if available,
+        otherwise return default
+        """
+        if self.isSigned(ns_uri, ns_key):
+            return self.message.getArg(ns_uri, ns_key, default)
+        else:
+            return default
+
+    def getSignedNS(self, ns_uri):
+        """Get signed arguments from the response message.  Return a
+        dict of all arguments in the specified namespace.  If any of
+        the arguments are not signed, return None.
+        """
+        msg_args = self.message.getArgs(ns_uri)
+
+        for key in msg_args.iterkeys():
+            if not self.isSigned(ns_uri, key):
+                oidutil.log("SuccessResponse.getSignedNS: (%s, %s) not signed."
+                            % (ns_uri, key))
+                return None
+
+        return msg_args
+
+    def extensionResponse(self, namespace_uri, require_signed):
+        """Return response arguments in the specified namespace.
+
+        @param namespace_uri: The namespace URI of the arguments to be
+        returned.
+
+        @param require_signed: True if the arguments should be among
+        those signed in the response, False if you don't care.
+
+        If require_signed is True and the arguments are not signed,
+        return None.
+        """
+        if require_signed:
+            return self.getSignedNS(namespace_uri)
+        else:
+            return self.message.getArgs(namespace_uri)
+
+    def getReturnTo(self):
+        """Get the openid.return_to argument from this response.
+
+        This is useful for verifying that this request was initiated
+        by this consumer.
+
+        @returns: The return_to URL supplied to the server on the
+            initial request, or C{None} if the response did not contain
+            an C{openid.return_to} argument.
+
+        @returntype: str
+        """
+        return self.getSigned(OPENID_NS, 'return_to')
+
+    def __eq__(self, other):
+        return (
+            (self.endpoint == other.endpoint) and
+            (self.identity_url == other.identity_url) and
+            (self.message == other.message) and
+            (self.signed_fields == other.signed_fields) and
+            (self.status == other.status))
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __repr__(self):
+        return '<%s.%s id=%r signed=%r>' % (
+            self.__class__.__module__,
+            self.__class__.__name__,
+            self.identity_url, self.signed_fields)
+
+
+class FailureResponse(Response):
+    """A response with a status of FAILURE. Indicates that the OpenID
+    protocol has failed. This could be locally or remotely triggered.
+
+    @ivar identity_url:  The identity URL for which authenitcation was
+        attempted, if it can be determined. Otherwise, None.
+
+    @ivar message: A message indicating why the request failed, if one
+        is supplied. otherwise, None.
+
+    @cvar status: FAILURE
+    """
+
+    status = FAILURE
+
+    def __init__(self, endpoint, message=None, contact=None,
+                 reference=None):
+        self.setEndpoint(endpoint)
+        self.message = message
+        self.contact = contact
+        self.reference = reference
+
+    def __repr__(self):
+        return "<%s.%s id=%r message=%r>" % (
+            self.__class__.__module__, self.__class__.__name__,
+            self.identity_url, self.message)
+
+
+class CancelResponse(Response):
+    """A response with a status of CANCEL. Indicates that the user
+    cancelled the OpenID authentication request.
+
+    @ivar identity_url: The identity URL for which authenitcation was
+        attempted, if it can be determined. Otherwise, None.
+
+    @cvar status: CANCEL
+    """
+
+    status = CANCEL
+
+    def __init__(self, endpoint):
+        self.setEndpoint(endpoint)
+
+class SetupNeededResponse(Response):
+    """A response with a status of SETUP_NEEDED. Indicates that the
+    request was in immediate mode, and the server is unable to
+    authenticate the user without further interaction.
+
+    @ivar identity_url:  The identity URL for which authenitcation was
+        attempted.
+
+    @ivar setup_url: A URL that can be used to send the user to the
+        server to set up for authentication. The user should be
+        redirected in to the setup_url, either in the current window
+        or in a new browser window.  C{None} in OpenID 2.0.
+
+    @cvar status: SETUP_NEEDED
+    """
+
+    status = SETUP_NEEDED
+
+    def __init__(self, endpoint, setup_url=None):
+        self.setEndpoint(endpoint)
+        self.setup_url = setup_url

+ 470 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/consumer/discover.py

@@ -0,0 +1,470 @@
+# -*- test-case-name: openid.test.test_discover -*-
+"""Functions to discover OpenID endpoints from identifiers.
+"""
+
+__all__ = [
+    'DiscoveryFailure',
+    'OPENID_1_0_NS',
+    'OPENID_1_0_TYPE',
+    'OPENID_1_1_TYPE',
+    'OPENID_2_0_TYPE',
+    'OPENID_IDP_2_0_TYPE',
+    'OpenIDServiceEndpoint',
+    'discover',
+    ]
+
+import urlparse
+
+from openid import oidutil, fetchers, urinorm
+
+from openid import yadis
+from openid.yadis.etxrd import nsTag, XRDSError, XRD_NS_2_0
+from openid.yadis.services import applyFilter as extractServices
+from openid.yadis.discover import discover as yadisDiscover
+from openid.yadis.discover import DiscoveryFailure
+from openid.yadis import xrires, filters
+from openid.yadis import xri
+
+from openid.consumer import html_parse
+
+OPENID_1_0_NS = 'http://openid.net/xmlns/1.0'
+OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server'
+OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon'
+OPENID_1_1_TYPE = 'http://openid.net/signon/1.1'
+OPENID_1_0_TYPE = 'http://openid.net/signon/1.0'
+
+from openid.message import OPENID1_NS as OPENID_1_0_MESSAGE_NS
+from openid.message import OPENID2_NS as OPENID_2_0_MESSAGE_NS
+
+class OpenIDServiceEndpoint(object):
+    """Object representing an OpenID service endpoint.
+
+    @ivar identity_url: the verified identifier.
+    @ivar canonicalID: For XRI, the persistent identifier.
+    """
+
+    # OpenID service type URIs, listed in order of preference.  The
+    # ordering of this list affects yadis and XRI service discovery.
+    openid_type_uris = [
+        OPENID_IDP_2_0_TYPE,
+
+        OPENID_2_0_TYPE,
+        OPENID_1_1_TYPE,
+        OPENID_1_0_TYPE,
+        ]
+
+    def __init__(self):
+        self.claimed_id = None
+        self.server_url = None
+        self.type_uris = []
+        self.local_id = None
+        self.canonicalID = None
+        self.used_yadis = False # whether this came from an XRDS
+        self.display_identifier = None
+
+    def usesExtension(self, extension_uri):
+        return extension_uri in self.type_uris
+
+    def preferredNamespace(self):
+        if (OPENID_IDP_2_0_TYPE in self.type_uris or
+            OPENID_2_0_TYPE in self.type_uris):
+            return OPENID_2_0_MESSAGE_NS
+        else:
+            return OPENID_1_0_MESSAGE_NS
+
+    def supportsType(self, type_uri):
+        """Does this endpoint support this type?
+
+        I consider C{/server} endpoints to implicitly support C{/signon}.
+        """
+        return (
+            (type_uri in self.type_uris) or 
+            (type_uri == OPENID_2_0_TYPE and self.isOPIdentifier())
+            )
+
+    def getDisplayIdentifier(self):
+        """Return the display_identifier if set, else return the claimed_id.
+        """
+        if self.display_identifier is not None:
+            return self.display_identifier
+        if self.claimed_id is None:
+            return None
+        else:
+            return urlparse.urldefrag(self.claimed_id)[0]
+
+    def compatibilityMode(self):
+        return self.preferredNamespace() != OPENID_2_0_MESSAGE_NS
+
+    def isOPIdentifier(self):
+        return OPENID_IDP_2_0_TYPE in self.type_uris
+
+    def parseService(self, yadis_url, uri, type_uris, service_element):
+        """Set the state of this object based on the contents of the
+        service element."""
+        self.type_uris = type_uris
+        self.server_url = uri
+        self.used_yadis = True
+
+        if not self.isOPIdentifier():
+            # XXX: This has crappy implications for Service elements
+            # that contain both 'server' and 'signon' Types.  But
+            # that's a pathological configuration anyway, so I don't
+            # think I care.
+            self.local_id = findOPLocalIdentifier(service_element,
+                                                  self.type_uris)
+            self.claimed_id = yadis_url
+
+    def getLocalID(self):
+        """Return the identifier that should be sent as the
+        openid.identity parameter to the server."""
+        # I looked at this conditional and thought "ah-hah! there's the bug!"
+        # but Python actually makes that one big expression somehow, i.e.
+        # "x is x is x" is not the same thing as "(x is x) is x".
+        # That's pretty weird, dude.  -- kmt, 1/07
+        if (self.local_id is self.canonicalID is None):
+            return self.claimed_id
+        else:
+            return self.local_id or self.canonicalID
+
+    def fromBasicServiceEndpoint(cls, endpoint):
+        """Create a new instance of this class from the endpoint
+        object passed in.
+
+        @return: None or OpenIDServiceEndpoint for this endpoint object"""
+        type_uris = endpoint.matchTypes(cls.openid_type_uris)
+
+        # If any Type URIs match and there is an endpoint URI
+        # specified, then this is an OpenID endpoint
+        if type_uris and endpoint.uri is not None:
+            openid_endpoint = cls()
+            openid_endpoint.parseService(
+                endpoint.yadis_url,
+                endpoint.uri,
+                endpoint.type_uris,
+                endpoint.service_element)
+        else:
+            openid_endpoint = None
+
+        return openid_endpoint
+
+    fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint)
+
+    def fromHTML(cls, uri, html):
+        """Parse the given document as HTML looking for an OpenID <link
+        rel=...>
+
+        @rtype: [OpenIDServiceEndpoint]
+        """
+        discovery_types = [
+            (OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'),
+            (OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'),
+            ]
+
+        link_attrs = html_parse.parseLinkAttrs(html)
+        services = []
+        for type_uri, op_endpoint_rel, local_id_rel in discovery_types:
+            op_endpoint_url = html_parse.findFirstHref(
+                link_attrs, op_endpoint_rel)
+            if op_endpoint_url is None:
+                continue
+
+            service = cls()
+            service.claimed_id = uri
+            service.local_id = html_parse.findFirstHref(
+                link_attrs, local_id_rel)
+            service.server_url = op_endpoint_url
+            service.type_uris = [type_uri]
+
+            services.append(service)
+
+        return services
+
+    fromHTML = classmethod(fromHTML)
+
+
+    def fromXRDS(cls, uri, xrds):
+        """Parse the given document as XRDS looking for OpenID services.
+
+        @rtype: [OpenIDServiceEndpoint]
+
+        @raises XRDSError: When the XRDS does not parse.
+
+        @since: 2.1.0
+        """
+        return extractServices(uri, xrds, cls)
+
+    fromXRDS = classmethod(fromXRDS)
+
+
+    def fromDiscoveryResult(cls, discoveryResult):
+        """Create endpoints from a DiscoveryResult.
+
+        @type discoveryResult: L{DiscoveryResult}
+
+        @rtype: list of L{OpenIDServiceEndpoint}
+
+        @raises XRDSError: When the XRDS does not parse.
+
+        @since: 2.1.0
+        """
+        if discoveryResult.isXRDS():
+            method = cls.fromXRDS
+        else:
+            method = cls.fromHTML
+        return method(discoveryResult.normalized_uri,
+                      discoveryResult.response_text)
+
+    fromDiscoveryResult = classmethod(fromDiscoveryResult)
+
+
+    def fromOPEndpointURL(cls, op_endpoint_url):
+        """Construct an OP-Identifier OpenIDServiceEndpoint object for
+        a given OP Endpoint URL
+
+        @param op_endpoint_url: The URL of the endpoint
+        @rtype: OpenIDServiceEndpoint
+        """
+        service = cls()
+        service.server_url = op_endpoint_url
+        service.type_uris = [OPENID_IDP_2_0_TYPE]
+        return service
+
+    fromOPEndpointURL = classmethod(fromOPEndpointURL)
+
+
+    def __str__(self):
+        return ("<%s.%s "
+                "server_url=%r "
+                "claimed_id=%r "
+                "local_id=%r "
+                "canonicalID=%r "
+                "used_yadis=%s "
+                ">"
+                 % (self.__class__.__module__, self.__class__.__name__,
+                    self.server_url,
+                    self.claimed_id,
+                    self.local_id,
+                    self.canonicalID,
+                    self.used_yadis))
+
+
+
+def findOPLocalIdentifier(service_element, type_uris):
+    """Find the OP-Local Identifier for this xrd:Service element.
+
+    This considers openid:Delegate to be a synonym for xrd:LocalID if
+    both OpenID 1.X and OpenID 2.0 types are present. If only OpenID
+    1.X is present, it returns the value of openid:Delegate. If only
+    OpenID 2.0 is present, it returns the value of xrd:LocalID. If
+    there is more than one LocalID tag and the values are different,
+    it raises a DiscoveryFailure. This is also triggered when the
+    xrd:LocalID and openid:Delegate tags are different.
+
+    @param service_element: The xrd:Service element
+    @type service_element: ElementTree.Node
+
+    @param type_uris: The xrd:Type values present in this service
+        element. This function could extract them, but higher level
+        code needs to do that anyway.
+    @type type_uris: [str]
+
+    @raises DiscoveryFailure: when discovery fails.
+
+    @returns: The OP-Local Identifier for this service element, if one
+        is present, or None otherwise.
+    @rtype: str or unicode or NoneType
+    """
+    # XXX: Test this function on its own!
+
+    # Build the list of tags that could contain the OP-Local Identifier
+    local_id_tags = []
+    if (OPENID_1_1_TYPE in type_uris or
+        OPENID_1_0_TYPE in type_uris):
+        local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate'))
+
+    if OPENID_2_0_TYPE in type_uris:
+        local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID'))
+
+    # Walk through all the matching tags and make sure that they all
+    # have the same value
+    local_id = None
+    for local_id_tag in local_id_tags:
+        for local_id_element in service_element.findall(local_id_tag):
+            if local_id is None:
+                local_id = local_id_element.text
+            elif local_id != local_id_element.text:
+                format = 'More than one %r tag found in one service element'
+                message = format % (local_id_tag,)
+                raise DiscoveryFailure(message, None)
+
+    return local_id
+
+def normalizeURL(url):
+    """Normalize a URL, converting normalization failures to
+    DiscoveryFailure"""
+    try:
+        normalized = urinorm.urinorm(url)
+    except ValueError, why:
+        raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None)
+    else:
+        return urlparse.urldefrag(normalized)[0]
+
+def normalizeXRI(xri):
+    """Normalize an XRI, stripping its scheme if present"""
+    if xri.startswith("xri://"):
+        xri = xri[6:]
+    return xri
+
+def arrangeByType(service_list, preferred_types):
+    """Rearrange service_list in a new list so services are ordered by
+    types listed in preferred_types.  Return the new list."""
+
+    def enumerate(elts):
+        """Return an iterable that pairs the index of an element with
+        that element.
+
+        For Python 2.2 compatibility"""
+        return zip(range(len(elts)), elts)
+
+    def bestMatchingService(service):
+        """Return the index of the first matching type, or something
+        higher if no type matches.
+
+        This provides an ordering in which service elements that
+        contain a type that comes earlier in the preferred types list
+        come before service elements that come later. If a service
+        element has more than one type, the most preferred one wins.
+        """
+        for i, t in enumerate(preferred_types):
+            if preferred_types[i] in service.type_uris:
+                return i
+
+        return len(preferred_types)
+
+    # Build a list with the service elements in tuples whose
+    # comparison will prefer the one with the best matching service
+    prio_services = [(bestMatchingService(s), orig_index, s)
+                     for (orig_index, s) in enumerate(service_list)]
+    prio_services.sort()
+
+    # Now that the services are sorted by priority, remove the sort
+    # keys from the list.
+    for i in range(len(prio_services)):
+        prio_services[i] = prio_services[i][2]
+
+    return prio_services
+
+def getOPOrUserServices(openid_services):
+    """Extract OP Identifier services.  If none found, return the
+    rest, sorted with most preferred first according to
+    OpenIDServiceEndpoint.openid_type_uris.
+
+    openid_services is a list of OpenIDServiceEndpoint objects.
+
+    Returns a list of OpenIDServiceEndpoint objects."""
+
+    op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE])
+
+    openid_services = arrangeByType(openid_services,
+                                    OpenIDServiceEndpoint.openid_type_uris)
+
+    return op_services or openid_services
+
+def discoverYadis(uri):
+    """Discover OpenID services for a URI. Tries Yadis and falls back
+    on old-style <link rel='...'> discovery if Yadis fails.
+
+    @param uri: normalized identity URL
+    @type uri: str
+
+    @return: (claimed_id, services)
+    @rtype: (str, list(OpenIDServiceEndpoint))
+
+    @raises DiscoveryFailure: when discovery fails.
+    """
+    # Might raise a yadis.discover.DiscoveryFailure if no document
+    # came back for that URI at all.  I don't think falling back
+    # to OpenID 1.0 discovery on the same URL will help, so don't
+    # bother to catch it.
+    response = yadisDiscover(uri)
+
+    yadis_url = response.normalized_uri
+    body = response.response_text
+    try:
+        openid_services = OpenIDServiceEndpoint.fromXRDS(yadis_url, body)
+    except XRDSError:
+        # Does not parse as a Yadis XRDS file
+        openid_services = []
+
+    if not openid_services:
+        # Either not an XRDS or there are no OpenID services.
+
+        if response.isXRDS():
+            # if we got the Yadis content-type or followed the Yadis
+            # header, re-fetch the document without following the Yadis
+            # header, with no Accept header.
+            return discoverNoYadis(uri)
+
+        # Try to parse the response as HTML.
+        # <link rel="...">
+        openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body)
+
+    return (yadis_url, getOPOrUserServices(openid_services))
+
+def discoverXRI(iname):
+    endpoints = []
+    iname = normalizeXRI(iname)
+    try:
+        canonicalID, services = xrires.ProxyResolver().query(
+            iname, OpenIDServiceEndpoint.openid_type_uris)
+
+        if canonicalID is None:
+            raise XRDSError('No CanonicalID found for XRI %r' % (iname,))
+
+        flt = filters.mkFilter(OpenIDServiceEndpoint)
+        for service_element in services:
+            endpoints.extend(flt.getServiceEndpoints(iname, service_element))
+    except XRDSError:
+        oidutil.log('xrds error on ' + iname)
+
+    for endpoint in endpoints:
+        # Is there a way to pass this through the filter to the endpoint
+        # constructor instead of tacking it on after?
+        endpoint.canonicalID = canonicalID
+        endpoint.claimed_id = canonicalID
+        endpoint.display_identifier = iname
+
+    # FIXME: returned xri should probably be in some normal form
+    return iname, getOPOrUserServices(endpoints)
+
+
+def discoverNoYadis(uri):
+    http_resp = fetchers.fetch(uri)
+    if http_resp.status not in (200, 206):
+        raise DiscoveryFailure(
+            'HTTP Response status from identity URL host is not 200. '
+            'Got status %r' % (http_resp.status,), http_resp)
+
+    claimed_id = http_resp.final_url
+    openid_services = OpenIDServiceEndpoint.fromHTML(
+        claimed_id, http_resp.body)
+    return claimed_id, openid_services
+
+def discoverURI(uri):
+    parsed = urlparse.urlparse(uri)
+    if parsed[0] and parsed[1]:
+        if parsed[0] not in ['http', 'https']:
+            raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None)
+    else:
+        uri = 'http://' + uri
+
+    uri = normalizeURL(uri)
+    claimed_id, openid_services = discoverYadis(uri)
+    claimed_id = normalizeURL(claimed_id)
+    return claimed_id, openid_services
+
+def discover(identifier):
+    if xri.identifierScheme(identifier) == "XRI":
+        return discoverXRI(identifier)
+    else:
+        return discoverURI(identifier)

+ 249 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/consumer/html_parse.py

@@ -0,0 +1,249 @@
+"""
+This module implements a VERY limited parser that finds <link> tags in
+the head of HTML or XHTML documents and parses out their attributes
+according to the OpenID spec. It is a liberal parser, but it requires
+these things from the data in order to work:
+
+ - There must be an open <html> tag
+
+ - There must be an open <head> tag inside of the <html> tag
+
+ - Only <link>s that are found inside of the <head> tag are parsed
+   (this is by design)
+
+ - The parser follows the OpenID specification in resolving the
+   attributes of the link tags. This means that the attributes DO NOT
+   get resolved as they would by an XML or HTML parser. In particular,
+   only certain entities get replaced, and href attributes do not get
+   resolved relative to a base URL.
+
+From http://openid.net/specs.bml#linkrel:
+
+ - The openid.server URL MUST be an absolute URL. OpenID consumers
+   MUST NOT attempt to resolve relative URLs.
+
+ - The openid.server URL MUST NOT include entities other than &amp;,
+   &lt;, &gt;, and &quot;.
+
+The parser ignores SGML comments and <![CDATA[blocks]]>. Both kinds of
+quoting are allowed for attributes.
+
+The parser deals with invalid markup in these ways:
+
+ - Tag names are not case-sensitive
+
+ - The <html> tag is accepted even when it is not at the top level
+
+ - The <head> tag is accepted even when it is not a direct child of
+   the <html> tag, but a <html> tag must be an ancestor of the <head>
+   tag
+
+ - <link> tags are accepted even when they are not direct children of
+   the <head> tag, but a <head> tag must be an ancestor of the <link>
+   tag
+
+ - If there is no closing tag for an open <html> or <head> tag, the
+   remainder of the document is viewed as being inside of the tag. If
+   there is no closing tag for a <link> tag, the link tag is treated
+   as a short tag. Exceptions to this rule are that <html> closes
+   <html> and <body> or <head> closes <head>
+
+ - Attributes of the <link> tag are not required to be quoted.
+
+ - In the case of duplicated attribute names, the attribute coming
+   last in the tag will be the value returned.
+
+ - Any text that does not parse as an attribute within a link tag will
+   be ignored. (e.g. <link pumpkin rel='openid.server' /> will ignore
+   pumpkin)
+
+ - If there are more than one <html> or <head> tag, the parser only
+   looks inside of the first one.
+
+ - The contents of <script> tags are ignored entirely, except unclosed
+   <script> tags. Unclosed <script> tags are ignored.
+
+ - Any other invalid markup is ignored, including unclosed SGML
+   comments and unclosed <![CDATA[blocks.
+"""
+
+__all__ = ['parseLinkAttrs']
+
+import re
+
+flags = ( re.DOTALL # Match newlines with '.'
+        | re.IGNORECASE
+        | re.VERBOSE # Allow comments and whitespace in patterns
+        | re.UNICODE # Make \b respect Unicode word boundaries
+        )
+
+# Stuff to remove before we start looking for tags
+removed_re = re.compile(r'''
+  # Comments
+  <!--.*?-->
+
+  # CDATA blocks
+| <!\[CDATA\[.*?\]\]>
+
+  # script blocks
+| <script\b
+
+  # make sure script is not an XML namespace
+  (?!:)
+
+  [^>]*>.*?</script>
+
+''', flags)
+
+tag_expr = r'''
+# Starts with the tag name at a word boundary, where the tag name is
+# not a namespace
+<%(tag_name)s\b(?!:)
+
+# All of the stuff up to a ">", hopefully attributes.
+(?P<attrs>[^>]*?)
+
+(?: # Match a short tag
+    />
+
+|   # Match a full tag
+    >
+
+    (?P<contents>.*?)
+
+    # Closed by
+    (?: # One of the specified close tags
+        </?%(closers)s\s*>
+
+        # End of the string
+    |   \Z
+
+    )
+
+)
+'''
+
+def tagMatcher(tag_name, *close_tags):
+    if close_tags:
+        options = '|'.join((tag_name,) + close_tags)
+        closers = '(?:%s)' % (options,)
+    else:
+        closers = tag_name
+
+    expr = tag_expr % locals()
+    return re.compile(expr, flags)
+
+# Must contain at least an open html and an open head tag
+html_find = tagMatcher('html')
+head_find = tagMatcher('head', 'body')
+link_find = re.compile(r'<link\b(?!:)', flags)
+
+attr_find = re.compile(r'''
+# Must start with a sequence of word-characters, followed by an equals sign
+(?P<attr_name>\w+)=
+
+# Then either a quoted or unquoted attribute
+(?:
+
+ # Match everything that\'s between matching quote marks
+ (?P<qopen>["\'])(?P<q_val>.*?)(?P=qopen)
+|
+
+ # If the value is not quoted, match up to whitespace
+ (?P<unq_val>(?:[^\s<>/]|/(?!>))+)
+)
+
+|
+
+(?P<end_link>[<>])
+''', flags)
+
+# Entity replacement:
+replacements = {
+    'amp':'&',
+    'lt':'<',
+    'gt':'>',
+    'quot':'"',
+    }
+
+ent_replace = re.compile(r'&(%s);' % '|'.join(replacements.keys()))
+def replaceEnt(mo):
+    "Replace the entities that are specified by OpenID"
+    return replacements.get(mo.group(1), mo.group())
+
+def parseLinkAttrs(html):
+    """Find all link tags in a string representing a HTML document and
+    return a list of their attributes.
+
+    @param html: the text to parse
+    @type html: str or unicode
+
+    @return: A list of dictionaries of attributes, one for each link tag
+    @rtype: [[(type(html), type(html))]]
+    """
+    stripped = removed_re.sub('', html)
+    html_mo = html_find.search(stripped)
+    if html_mo is None or html_mo.start('contents') == -1:
+        return []
+
+    start, end = html_mo.span('contents')
+    head_mo = head_find.search(stripped, start, end)
+    if head_mo is None or head_mo.start('contents') == -1:
+        return []
+
+    start, end = head_mo.span('contents')
+    link_mos = link_find.finditer(stripped, head_mo.start(), head_mo.end())
+
+    matches = []
+    for link_mo in link_mos:
+        start = link_mo.start() + 5
+        link_attrs = {}
+        for attr_mo in attr_find.finditer(stripped, start):
+            if attr_mo.lastgroup == 'end_link':
+                break
+
+            # Either q_val or unq_val must be present, but not both
+            # unq_val is a True (non-empty) value if it is present
+            attr_name, q_val, unq_val = attr_mo.group(
+                'attr_name', 'q_val', 'unq_val')
+            attr_val = ent_replace.sub(replaceEnt, unq_val or q_val)
+
+            link_attrs[attr_name] = attr_val
+
+        matches.append(link_attrs)
+
+    return matches
+
+def relMatches(rel_attr, target_rel):
+    """Does this target_rel appear in the rel_str?"""
+    # XXX: TESTME
+    rels = rel_attr.strip().split()
+    for rel in rels:
+        rel = rel.lower()
+        if rel == target_rel:
+            return 1
+
+    return 0
+
+def linkHasRel(link_attrs, target_rel):
+    """Does this link have target_rel as a relationship?"""
+    # XXX: TESTME
+    rel_attr = link_attrs.get('rel')
+    return rel_attr and relMatches(rel_attr, target_rel)
+
+def findLinksRel(link_attrs_list, target_rel):
+    """Filter the list of link attributes on whether it has target_rel
+    as a relationship."""
+    # XXX: TESTME
+    matchesTarget = lambda attrs: linkHasRel(attrs, target_rel)
+    return filter(matchesTarget, link_attrs_list)
+
+def findFirstHref(link_attrs_list, target_rel):
+    """Return the value of the href attribute for the first link tag
+    in the list that has target_rel as a relationship."""
+    # XXX: TESTME
+    matches = findLinksRel(link_attrs_list, target_rel)
+    if not matches:
+        return None
+    first = matches[0]
+    return first.get('href')

+ 220 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/cryptutil.py

@@ -0,0 +1,220 @@
+"""Module containing a cryptographic-quality source of randomness and
+other cryptographically useful functionality
+
+Python 2.4 needs no external support for this module, nor does Python
+2.3 on a system with /dev/urandom.
+
+Other configurations will need a quality source of random bytes and
+access to a function that will convert binary strings to long
+integers. This module will work with the Python Cryptography Toolkit
+(pycrypto) if it is present. pycrypto can be found with a search
+engine, but is currently found at:
+
+http://www.amk.ca/python/code/crypto
+"""
+
+__all__ = [
+    'base64ToLong',
+    'binaryToLong',
+    'hmacSha1',
+    'hmacSha256',
+    'longToBase64',
+    'longToBinary',
+    'randomString',
+    'randrange',
+    'sha1',
+    'sha256',
+    ]
+
+import hmac
+import os
+import random
+
+from openid.oidutil import toBase64, fromBase64
+
+try:
+    import hashlib
+except ImportError:
+    import sha as sha1_module
+
+    try:
+        from Crypto.Hash import SHA256 as sha256_module
+    except ImportError:
+        sha256_module = None
+
+else:
+    class HashContainer(object):
+        def __init__(self, hash_constructor):
+            self.new = hash_constructor
+            self.digest_size = hash_constructor().digest_size
+
+    sha1_module = HashContainer(hashlib.sha1)
+    sha256_module = HashContainer(hashlib.sha256)
+
+def hmacSha1(key, text):
+    return hmac.new(key, text, sha1_module).digest()
+
+def sha1(s):
+    return sha1_module.new(s).digest()
+
+if sha256_module is not None:
+    def hmacSha256(key, text):
+        return hmac.new(key, text, sha256_module).digest()
+
+    def sha256(s):
+        return sha256_module.new(s).digest()
+
+    SHA256_AVAILABLE = True
+
+else:
+    _no_sha256 = NotImplementedError(
+        'Use Python 2.5, install pycrypto or install hashlib to use SHA256')
+
+    def hmacSha256(unused_key, unused_text):
+        raise _no_sha256
+
+    def sha256(s):
+        raise _no_sha256
+
+    SHA256_AVAILABLE = False
+
+try:
+    from Crypto.Util.number import long_to_bytes, bytes_to_long
+except ImportError:
+    import pickle
+    try:
+        # Check Python compatiblity by raising an exception on import
+        # if the needed functionality is not present. Present in
+        # Python >= 2.3
+        pickle.encode_long
+        pickle.decode_long
+    except AttributeError:
+        raise ImportError(
+            'No functionality for serializing long integers found')
+
+    # Present in Python >= 2.4
+    try:
+        reversed
+    except NameError:
+        def reversed(seq):
+            return map(seq.__getitem__, xrange(len(seq) - 1, -1, -1))
+
+    def longToBinary(l):
+        if l == 0:
+            return '\x00'
+
+        return ''.join(reversed(pickle.encode_long(l)))
+
+    def binaryToLong(s):
+        return pickle.decode_long(''.join(reversed(s)))
+else:
+    # We have pycrypto
+
+    def longToBinary(l):
+        if l < 0:
+            raise ValueError('This function only supports positive integers')
+
+        bytes = long_to_bytes(l)
+        if ord(bytes[0]) > 127:
+            return '\x00' + bytes
+        else:
+            return bytes
+
+    def binaryToLong(bytes):
+        if not bytes:
+            raise ValueError('Empty string passed to strToLong')
+
+        if ord(bytes[0]) > 127:
+            raise ValueError('This function only supports positive integers')
+
+        return bytes_to_long(bytes)
+
+# A cryptographically safe source of random bytes
+try:
+    getBytes = os.urandom
+except AttributeError:
+    try:
+        from Crypto.Util.randpool import RandomPool
+    except ImportError:
+        # Fall back on /dev/urandom, if present. It would be nice to
+        # have Windows equivalent here, but for now, require pycrypto
+        # on Windows.
+        try:
+            _urandom = file('/dev/urandom', 'rb')
+        except IOError:
+            raise ImportError('No adequate source of randomness found!')
+        else:
+            def getBytes(n):
+                bytes = []
+                while n:
+                    chunk = _urandom.read(n)
+                    n -= len(chunk)
+                    bytes.append(chunk)
+                    assert n >= 0
+                return ''.join(bytes)
+    else:
+        _pool = RandomPool()
+        def getBytes(n, pool=_pool):
+            if pool.entropy < n:
+                pool.randomize()
+            return pool.get_bytes(n)
+
+# A randrange function that works for longs
+try:
+    randrange = random.SystemRandom().randrange
+except AttributeError:
+    # In Python 2.2's random.Random, randrange does not support
+    # numbers larger than sys.maxint for randrange. For simplicity,
+    # use this implementation for any Python that does not have
+    # random.SystemRandom
+    from math import log, ceil
+
+    _duplicate_cache = {}
+    def randrange(start, stop=None, step=1):
+        if stop is None:
+            stop = start
+            start = 0
+
+        r = (stop - start) // step
+        try:
+            (duplicate, nbytes) = _duplicate_cache[r]
+        except KeyError:
+            rbytes = longToBinary(r)
+            if rbytes[0] == '\x00':
+                nbytes = len(rbytes) - 1
+            else:
+                nbytes = len(rbytes)
+
+            mxrand = (256 ** nbytes)
+
+            # If we get a number less than this, then it is in the
+            # duplicated range.
+            duplicate = mxrand % r
+
+            if len(_duplicate_cache) > 10:
+                _duplicate_cache.clear()
+
+            _duplicate_cache[r] = (duplicate, nbytes)
+
+        while 1:
+            bytes = '\x00' + getBytes(nbytes)
+            n = binaryToLong(bytes)
+            # Keep looping if this value is in the low duplicated range
+            if n >= duplicate:
+                break
+
+        return start + (n % r) * step
+
+def longToBase64(l):
+    return toBase64(longToBinary(l))
+
+def base64ToLong(s):
+    return binaryToLong(fromBase64(s))
+
+def randomString(length, chrs=None):
+    """Produce a string of length random bytes, chosen from chrs."""
+    if chrs is None:
+        return getBytes(length)
+    else:
+        n = len(chrs)
+        return ''.join([chrs[randrange(n)] for _ in xrange(length)])

+ 42 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/dh.py

@@ -0,0 +1,42 @@
+from openid import cryptutil
+from openid import oidutil
+
+def strxor(x, y):
+    if len(x) != len(y):
+        raise ValueError('Inputs to strxor must have the same length')
+
+    xor = lambda (a, b): chr(ord(a) ^ ord(b))
+    return "".join(map(xor, zip(x, y)))
+
+class DiffieHellman(object):
+    DEFAULT_MOD = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443L
+
+    DEFAULT_GEN = 2
+
+    def fromDefaults(cls):
+        return cls(cls.DEFAULT_MOD, cls.DEFAULT_GEN)
+
+    fromDefaults = classmethod(fromDefaults)
+
+    def __init__(self, modulus, generator):
+        self.modulus = long(modulus)
+        self.generator = long(generator)
+
+        self._setPrivate(cryptutil.randrange(1, modulus - 1))
+
+    def _setPrivate(self, private):
+        """This is here to make testing easier"""
+        self.private = private
+        self.public = pow(self.generator, self.private, self.modulus)
+
+    def usingDefaultValues(self):
+        return (self.modulus == self.DEFAULT_MOD and
+                self.generator == self.DEFAULT_GEN)
+
+    def getSharedSecret(self, composite):
+        return pow(composite, self.private, self.modulus)
+
+    def xorSecret(self, composite, secret, hash_func):
+        dh_shared = self.getSharedSecret(composite)
+        hashed_dh_shared = hash_func(cryptutil.longToBinary(dh_shared))
+        return strxor(secret, hashed_dh_shared)

+ 46 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/extension.py

@@ -0,0 +1,46 @@
+from openid import message as message_module
+
+class Extension(object):
+    """An interface for OpenID extensions.
+
+    @ivar ns_uri: The namespace to which to add the arguments for this
+        extension
+    """
+    ns_uri = None
+    ns_alias = None
+
+    def getExtensionArgs(self):
+        """Get the string arguments that should be added to an OpenID
+        message for this extension.
+
+        @returns: A dictionary of completely non-namespaced arguments
+            to be added. For example, if the extension's alias is
+            'uncle', and this method returns {'meat':'Hot Rats'}, the
+            final message will contain {'openid.uncle.meat':'Hot Rats'}
+        """
+        raise NotImplementedError
+
+    def toMessage(self, message=None):
+        """Add the arguments from this extension to the provided
+        message, or create a new message containing only those
+        arguments.
+
+        @returns: The message with the extension arguments added
+        """
+        if message is None:
+            warnings.warn('Passing None to Extension.toMessage is deprecated. '
+                          'Creating a message assuming you want OpenID 2.',
+                          DeprecationWarning, stacklevel=2)
+            message = message_module.Message(message_module.OPENID2_NS)
+
+        implicit = message.isOpenID1()
+
+        try:
+            message.namespaces.addAlias(self.ns_uri, self.ns_alias,
+                                        implicit=implicit)
+        except KeyError:
+            if message.namespaces.getAlias(self.ns_uri) != self.ns_alias:
+                raise
+
+        message.updateArgs(self.ns_uri, self.getExtensionArgs())
+        return message

+ 5 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/extensions/__init__.py

@@ -0,0 +1,5 @@
+"""OpenID Extension modules."""
+
+__all__ = ['ax', 'pape', 'sreg']
+
+from openid.extensions.draft import pape5 as pape

+ 774 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/extensions/ax.py

@@ -0,0 +1,774 @@
+# -*- 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'
+    mode = None
+    ns_uri = 'http://openid.net/srv/ax/1.0'
+
+    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 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.iteritems():
+            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.iteritems():
+            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, 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.iteritems():
+            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 self.requested_attributes.itervalues()
+
+    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.iteritems():
+            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.iteritems():
+            if key.startswith('type.'):
+                type_uri = value
+                alias = key[5:]
+                checkAlias(alias)
+                aliases.addAlias(type_uri, alias)
+
+        for type_uri, alias in aliases.iteritems():
+            try:
+                count_s = ax_args['count.' + alias]
+            except KeyError:
+                value = ax_args['value.' + alias]
+
+                if value == u'':
+                    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, 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

+ 277 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape2.py

@@ -0,0 +1,277 @@
+"""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 __nonzero__(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:
+            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 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 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

+ 473 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape5.py

@@ -0,0 +1,473 @@
+"""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 xrange(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.iteritems():
+            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 __nonzero__(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:
+            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 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.iteritems():
+            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.iteritems():
+            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.iteritems():
+            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

+ 518 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/extensions/sreg.py

@@ -0,0 +1,518 @@
+"""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
+from openid import oidutil
+
+try:
+    basestring #pylint:disable-msg=W0104
+except NameError:
+    # For Python 2.2
+    basestring = (str, unicode) #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, e:
+    oidutil.log('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, why:
+            # An alias for the string 'sreg' already exists, but it's
+            # defined for something other than simple registration
+            raise SRegNamespaceError(why[0])
+
+    # 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, basestring):
+            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 self.data.items()
+
+    def iteritems(self):
+        return self.data.iteritems()
+
+    def keys(self):
+        return self.data.keys()
+
+    def iterkeys(self):
+        return self.data.iterkeys()
+
+    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 __nonzero__(self):
+        return bool(self.data)

+ 427 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/fetchers.py

@@ -0,0 +1,427 @@
+# -*- test-case-name: openid.test.test_fetchers -*-
+"""
+This module contains the HTTP fetcher interface and several implementations.
+"""
+
+__all__ = ['fetch', 'getDefaultFetcher', 'setDefaultFetcher', 'HTTPResponse',
+           'HTTPFetcher', 'createHTTPFetcher', 'HTTPFetchingError',
+           'HTTPError']
+
+import urllib2
+import time
+import cStringIO
+import sys
+
+import openid
+import openid.urinorm
+
+# Try to import httplib2 for caching support
+# http://bitworking.org/projects/httplib2/
+try:
+    import httplib2
+except ImportError:
+    # httplib2 not available
+    httplib2 = None
+
+# try to import pycurl, which will let us use CurlHTTPFetcher
+try:
+    import pycurl
+except ImportError:
+    pycurl = None
+
+USER_AGENT = "python-openid/%s (%s)" % (openid.__version__, sys.platform)
+MAX_RESPONSE_KB = 1024
+
+def fetch(url, body=None, headers=None):
+    """Invoke the fetch method on the default fetcher. Most users
+    should need only this method.
+
+    @raises Exception: any exceptions that may be raised by the default fetcher
+    """
+    fetcher = getDefaultFetcher()
+    return fetcher.fetch(url, body, headers)
+
+def createHTTPFetcher():
+    """Create a default HTTP fetcher instance
+
+    prefers Curl to urllib2."""
+    if pycurl is None:
+        fetcher = Urllib2Fetcher()
+    else:
+        fetcher = CurlHTTPFetcher()
+
+    return fetcher
+
+# Contains the currently set HTTP fetcher. If it is set to None, the
+# library will call createHTTPFetcher() to set it. Do not access this
+# variable outside of this module.
+_default_fetcher = None
+
+def getDefaultFetcher():
+    """Return the default fetcher instance
+    if no fetcher has been set, it will create a default fetcher.
+
+    @return: the default fetcher
+    @rtype: HTTPFetcher
+    """
+    global _default_fetcher
+
+    if _default_fetcher is None:
+        setDefaultFetcher(createHTTPFetcher())
+
+    return _default_fetcher
+
+def setDefaultFetcher(fetcher, wrap_exceptions=True):
+    """Set the default fetcher
+
+    @param fetcher: The fetcher to use as the default HTTP fetcher
+    @type fetcher: HTTPFetcher
+
+    @param wrap_exceptions: Whether to wrap exceptions thrown by the
+        fetcher wil HTTPFetchingError so that they may be caught
+        easier. By default, exceptions will be wrapped. In general,
+        unwrapped fetchers are useful for debugging of fetching errors
+        or if your fetcher raises well-known exceptions that you would
+        like to catch.
+    @type wrap_exceptions: bool
+    """
+    global _default_fetcher
+    if fetcher is None or not wrap_exceptions:
+        _default_fetcher = fetcher
+    else:
+        _default_fetcher = ExceptionWrappingFetcher(fetcher)
+
+def usingCurl():
+    """Whether the currently set HTTP fetcher is a Curl HTTP fetcher."""
+    return isinstance(getDefaultFetcher(), CurlHTTPFetcher)
+
+class HTTPResponse(object):
+    """XXX document attributes"""
+    headers = None
+    status = None
+    body = None
+    final_url = None
+
+    def __init__(self, final_url=None, status=None, headers=None, body=None):
+        self.final_url = final_url
+        self.status = status
+        self.headers = headers
+        self.body = body
+
+    def __repr__(self):
+        return "<%s status %s for %s>" % (self.__class__.__name__,
+                                          self.status,
+                                          self.final_url)
+
+class HTTPFetcher(object):
+    """
+    This class is the interface for openid HTTP fetchers.  This
+    interface is only important if you need to write a new fetcher for
+    some reason.
+    """
+
+    def fetch(self, url, body=None, headers=None):
+        """
+        This performs an HTTP POST or GET, following redirects along
+        the way. If a body is specified, then the request will be a
+        POST. Otherwise, it will be a GET.
+
+
+        @param headers: HTTP headers to include with the request
+        @type headers: {str:str}
+
+        @return: An object representing the server's HTTP response. If
+            there are network or protocol errors, an exception will be
+            raised. HTTP error responses, like 404 or 500, do not
+            cause exceptions.
+
+        @rtype: L{HTTPResponse}
+
+        @raise Exception: Different implementations will raise
+            different errors based on the underlying HTTP library.
+        """
+        raise NotImplementedError
+
+def _allowedURL(url):
+    return url.startswith('http://') or url.startswith('https://')
+
+class HTTPFetchingError(Exception):
+    """Exception that is wrapped around all exceptions that are raised
+    by the underlying fetcher when using the ExceptionWrappingFetcher
+
+    @ivar why: The exception that caused this exception
+    """
+    def __init__(self, why=None):
+        Exception.__init__(self, why)
+        self.why = why
+
+class ExceptionWrappingFetcher(HTTPFetcher):
+    """Fetcher that wraps another fetcher, causing all exceptions
+
+    @cvar uncaught_exceptions: Exceptions that should be exposed to the
+        user if they are raised by the fetch call
+    """
+
+    uncaught_exceptions = (SystemExit, KeyboardInterrupt, MemoryError)
+
+    def __init__(self, fetcher):
+        self.fetcher = fetcher
+
+    def fetch(self, *args, **kwargs):
+        try:
+            return self.fetcher.fetch(*args, **kwargs)
+        except self.uncaught_exceptions:
+            raise
+        except:
+            exc_cls, exc_inst = sys.exc_info()[:2]
+            if exc_inst is None:
+                # string exceptions
+                exc_inst = exc_cls
+
+            raise HTTPFetchingError(why=exc_inst)
+
+class Urllib2Fetcher(HTTPFetcher):
+    """An C{L{HTTPFetcher}} that uses urllib2.
+    """
+
+    # Parameterized for the benefit of testing frameworks, see
+    # http://trac.openidenabled.com/trac/ticket/85
+    urlopen = staticmethod(urllib2.urlopen)
+
+    def fetch(self, url, body=None, headers=None):
+        if not _allowedURL(url):
+            raise ValueError('Bad URL scheme: %r' % (url,))
+
+        if headers is None:
+            headers = {}
+
+        headers.setdefault(
+            'User-Agent',
+            "%s Python-urllib/%s" % (USER_AGENT, urllib2.__version__,))
+
+        req = urllib2.Request(url, data=body, headers=headers)
+        try:
+            f = self.urlopen(req)
+            try:
+                return self._makeResponse(f)
+            finally:
+                f.close()
+        except urllib2.HTTPError, why:
+            try:
+                return self._makeResponse(why)
+            finally:
+                why.close()
+
+    def _makeResponse(self, urllib2_response):
+        resp = HTTPResponse()
+        resp.body = urllib2_response.read(MAX_RESPONSE_KB * 1024)
+        resp.final_url = urllib2_response.geturl()
+        resp.headers = dict(urllib2_response.info().items())
+
+        if hasattr(urllib2_response, 'code'):
+            resp.status = urllib2_response.code
+        else:
+            resp.status = 200
+
+        return resp
+
+class HTTPError(HTTPFetchingError):
+    """
+    This exception is raised by the C{L{CurlHTTPFetcher}} when it
+    encounters an exceptional situation fetching a URL.
+    """
+    pass
+
+# XXX: define what we mean by paranoid, and make sure it is.
+class CurlHTTPFetcher(HTTPFetcher):
+    """
+    An C{L{HTTPFetcher}} that uses pycurl for fetching.
+    See U{http://pycurl.sourceforge.net/}.
+    """
+    ALLOWED_TIME = 20 # seconds
+
+    def __init__(self):
+        HTTPFetcher.__init__(self)
+        if pycurl is None:
+            raise RuntimeError('Cannot find pycurl library')
+
+    def _parseHeaders(self, header_file):
+        header_file.seek(0)
+
+        # Remove the status line from the beginning of the input
+        unused_http_status_line = header_file.readline().lower ()
+        if unused_http_status_line.startswith('http/1.1 100 '):
+            unused_http_status_line = header_file.readline()
+            unused_http_status_line = header_file.readline()
+
+        lines = [line.strip() for line in header_file]
+
+        # and the blank line from the end
+        empty_line = lines.pop()
+        if empty_line:
+            raise HTTPError("No blank line at end of headers: %r" % (line,))
+
+        headers = {}
+        for line in lines:
+            try:
+                name, value = line.split(':', 1)
+            except ValueError:
+                raise HTTPError(
+                    "Malformed HTTP header line in response: %r" % (line,))
+
+            value = value.strip()
+
+            # HTTP headers are case-insensitive
+            name = name.lower()
+            headers[name] = value
+
+        return headers
+
+    def _checkURL(self, url):
+        # XXX: document that this can be overridden to match desired policy
+        # XXX: make sure url is well-formed and routeable
+        return _allowedURL(url)
+
+    def fetch(self, url, body=None, headers=None):
+        stop = int(time.time()) + self.ALLOWED_TIME
+        off = self.ALLOWED_TIME
+
+        if headers is None:
+            headers = {}
+
+        headers.setdefault('User-Agent',
+                           "%s %s" % (USER_AGENT, pycurl.version,))
+
+        header_list = []
+        if headers is not None:
+            for header_name, header_value in headers.iteritems():
+                header_list.append('%s: %s' % (header_name, header_value))
+
+        c = pycurl.Curl()
+        try:
+            c.setopt(pycurl.NOSIGNAL, 1)
+
+            if header_list:
+                c.setopt(pycurl.HTTPHEADER, header_list)
+
+            # Presence of a body indicates that we should do a POST
+            if body is not None:
+                c.setopt(pycurl.POST, 1)
+                c.setopt(pycurl.POSTFIELDS, body)
+
+            while off > 0:
+                if not self._checkURL(url):
+                    raise HTTPError("Fetching URL not allowed: %r" % (url,))
+
+                data = cStringIO.StringIO()
+                def write_data(chunk):
+                    if data.tell() > 1024*MAX_RESPONSE_KB:
+                        return 0
+                    else:
+                        return data.write(chunk)
+                    
+                response_header_data = cStringIO.StringIO()
+                c.setopt(pycurl.WRITEFUNCTION, write_data)
+                c.setopt(pycurl.HEADERFUNCTION, response_header_data.write)
+                c.setopt(pycurl.TIMEOUT, off)
+                c.setopt(pycurl.URL, openid.urinorm.urinorm(url))
+
+                c.perform()
+
+                response_headers = self._parseHeaders(response_header_data)
+                code = c.getinfo(pycurl.RESPONSE_CODE)
+                if code in [301, 302, 303, 307]:
+                    url = response_headers.get('location')
+                    if url is None:
+                        raise HTTPError(
+                            'Redirect (%s) returned without a location' % code)
+
+                    # Redirects are always GETs
+                    c.setopt(pycurl.POST, 0)
+
+                    # There is no way to reset POSTFIELDS to empty and
+                    # reuse the connection, but we only use it once.
+                else:
+                    resp = HTTPResponse()
+                    resp.headers = response_headers
+                    resp.status = code
+                    resp.final_url = url
+                    resp.body = data.getvalue()
+                    return resp
+
+                off = stop - int(time.time())
+
+            raise HTTPError("Timed out fetching: %r" % (url,))
+        finally:
+            c.close()
+
+class HTTPLib2Fetcher(HTTPFetcher):
+    """A fetcher that uses C{httplib2} for performing HTTP
+    requests. This implementation supports HTTP caching.
+
+    @see: http://bitworking.org/projects/httplib2/
+    """
+
+    def __init__(self, cache=None):
+        """@param cache: An object suitable for use as an C{httplib2}
+            cache. If a string is passed, it is assumed to be a
+            directory name.
+        """
+        if httplib2 is None:
+            raise RuntimeError('Cannot find httplib2 library. '
+                               'See http://bitworking.org/projects/httplib2/')
+
+        super(HTTPLib2Fetcher, self).__init__()
+
+        # An instance of the httplib2 object that performs HTTP requests
+        self.httplib2 = httplib2.Http(cache)
+
+        # We want httplib2 to raise exceptions for errors, just like
+        # the other fetchers.
+        self.httplib2.force_exception_to_status_code = False
+
+    def fetch(self, url, body=None, headers=None):
+        """Perform an HTTP request
+
+        @raises Exception: Any exception that can be raised by httplib2
+
+        @see: C{L{HTTPFetcher.fetch}}
+        """
+        if body:
+            method = 'POST'
+        else:
+            method = 'GET'
+
+        if headers is None:
+            headers = {}
+
+        # httplib2 doesn't check to make sure that the URL's scheme is
+        # 'http' so we do it here.
+        if not (url.startswith('http://') or url.startswith('https://')):
+            raise ValueError('URL is not a HTTP URL: %r' % (url,))
+
+        httplib2_response, content = self.httplib2.request(
+            url, method, body=body, headers=headers)
+
+        # Translate the httplib2 response to our HTTP response abstraction
+
+        # When a 400 is returned, there is no "content-location"
+        # header set. This seems like a bug to me. I can't think of a
+        # case where we really care about the final URL when it is an
+        # error response, but being careful about it can't hurt.
+        try:
+            final_url = httplib2_response['content-location']
+        except KeyError:
+            # We're assuming that no redirects occurred
+            assert not httplib2_response.previous
+
+            # And this should never happen for a successful response
+            assert httplib2_response.status != 200
+            final_url = url
+
+        return HTTPResponse(
+            body=content,
+            final_url=final_url,
+            headers=dict(httplib2_response.items()),
+            status=httplib2_response.status,
+            )

+ 123 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/kvform.py

@@ -0,0 +1,123 @@
+__all__ = ['seqToKV', 'kvToSeq', 'dictToKV', 'kvToDict']
+
+from openid import oidutil
+
+import types
+
+class KVFormError(ValueError):
+    pass
+
+def seqToKV(seq, strict=False):
+    """Represent a sequence of pairs of strings as newline-terminated
+    key:value pairs. The pairs are generated in the order given.
+
+    @param seq: The pairs
+    @type seq: [(str, (unicode|str))]
+
+    @return: A string representation of the sequence
+    @rtype: str
+    """
+    def err(msg):
+        formatted = 'seqToKV warning: %s: %r' % (msg, seq)
+        if strict:
+            raise KVFormError(formatted)
+        else:
+            oidutil.log(formatted)
+
+    lines = []
+    for k, v in seq:
+        if isinstance(k, types.StringType):
+            k = k.decode('UTF8')
+        elif not isinstance(k, types.UnicodeType):
+            err('Converting key to string: %r' % k)
+            k = str(k)
+
+        if '\n' in k:
+            raise KVFormError(
+                'Invalid input for seqToKV: key contains newline: %r' % (k,))
+
+        if ':' in k:
+            raise KVFormError(
+                'Invalid input for seqToKV: key contains colon: %r' % (k,))
+
+        if k.strip() != k:
+            err('Key has whitespace at beginning or end: %r' % (k,))
+
+        if isinstance(v, types.StringType):
+            v = v.decode('UTF8')
+        elif not isinstance(v, types.UnicodeType):
+            err('Converting value to string: %r' % (v,))
+            v = str(v)
+
+        if '\n' in v:
+            raise KVFormError(
+                'Invalid input for seqToKV: value contains newline: %r' % (v,))
+
+        if v.strip() != v:
+            err('Value has whitespace at beginning or end: %r' % (v,))
+
+        lines.append(k + ':' + v + '\n')
+
+    return ''.join(lines).encode('UTF8')
+
+def kvToSeq(data, strict=False):
+    """
+
+    After one parse, seqToKV and kvToSeq are inverses, with no warnings::
+
+        seq = kvToSeq(s)
+        seqToKV(kvToSeq(seq)) == seq
+    """
+    def err(msg):
+        formatted = 'kvToSeq warning: %s: %r' % (msg, data)
+        if strict:
+            raise KVFormError(formatted)
+        else:
+            oidutil.log(formatted)
+
+    lines = data.split('\n')
+    if lines[-1]:
+        err('Does not end in a newline')
+    else:
+        del lines[-1]
+
+    pairs = []
+    line_num = 0
+    for line in lines:
+        line_num += 1
+
+        # Ignore blank lines
+        if not line.strip():
+            continue
+
+        pair = line.split(':', 1)
+        if len(pair) == 2:
+            k, v = pair
+            k_s = k.strip()
+            if k_s != k:
+                fmt = ('In line %d, ignoring leading or trailing '
+                       'whitespace in key %r')
+                err(fmt % (line_num, k))
+
+            if not k_s:
+                err('In line %d, got empty key' % (line_num,))
+
+            v_s = v.strip()
+            if v_s != v:
+                fmt = ('In line %d, ignoring leading or trailing '
+                       'whitespace in value %r')
+                err(fmt % (line_num, v))
+
+            pairs.append((k_s.decode('UTF8'), v_s.decode('UTF8')))
+        else:
+            err('Line %d does not contain a colon' % line_num)
+
+    return pairs
+
+def dictToKV(d):
+    seq = d.items()
+    seq.sort()
+    return seqToKV(seq)
+
+def kvToDict(s):
+    return dict(kvToSeq(s))

+ 631 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/message.py

@@ -0,0 +1,631 @@
+"""Extension argument processing code
+"""
+__all__ = ['Message', 'NamespaceMap', 'no_default', 'registerNamespaceAlias',
+           'OPENID_NS', 'BARE_NS', 'OPENID1_NS', 'OPENID2_NS', 'SREG_URI',
+           'IDENTIFIER_SELECT']
+
+import copy
+import warnings
+import urllib
+
+from openid import oidutil
+from openid import kvform
+try:
+    ElementTree = oidutil.importElementTree()
+except ImportError:
+    # No elementtree found, so give up, but don't fail to import,
+    # since we have fallbacks.
+    ElementTree = None
+
+# This doesn't REALLY belong here, but where is better?
+IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select'
+
+# URI for Simple Registration extension, the only commonly deployed
+# OpenID 1.x extension, and so a special case
+SREG_URI = 'http://openid.net/sreg/1.0'
+
+# The OpenID 1.X namespace URI
+OPENID1_NS = 'http://openid.net/signon/1.0'
+THE_OTHER_OPENID1_NS = 'http://openid.net/signon/1.1'
+
+OPENID1_NAMESPACES = OPENID1_NS, THE_OTHER_OPENID1_NS
+
+# The OpenID 2.0 namespace URI
+OPENID2_NS = 'http://specs.openid.net/auth/2.0'
+
+# The namespace consisting of pairs with keys that are prefixed with
+# "openid."  but not in another namespace.
+NULL_NAMESPACE = oidutil.Symbol('Null namespace')
+
+# The null namespace, when it is an allowed OpenID namespace
+OPENID_NS = oidutil.Symbol('OpenID namespace')
+
+# The top-level namespace, excluding all pairs with keys that start
+# with "openid."
+BARE_NS = oidutil.Symbol('Bare namespace')
+
+# Limit, in bytes, of identity provider and return_to URLs, including
+# response payload.  See OpenID 1.1 specification, Appendix D.
+OPENID1_URL_LIMIT = 2047
+
+# All OpenID protocol fields.  Used to check namespace aliases.
+OPENID_PROTOCOL_FIELDS = [
+    'ns', 'mode', 'error', 'return_to', 'contact', 'reference',
+    'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen',
+    'dh_consumer_public', 'claimed_id', 'identity', 'realm',
+    'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig',
+    'assoc_handle', 'trust_root', 'openid',
+    ]
+
+class UndefinedOpenIDNamespace(ValueError):
+    """Raised if the generic OpenID namespace is accessed when there
+    is no OpenID namespace set for this message."""
+
+class InvalidOpenIDNamespace(ValueError):
+    """Raised if openid.ns is not a recognized value.
+
+    For recognized values, see L{Message.allowed_openid_namespaces}
+    """
+    def __str__(self):
+        s = "Invalid OpenID Namespace"
+        if self.args:
+            s += " %r" % (self.args[0],)
+        return s
+
+
+# Sentinel used for Message implementation to indicate that getArg
+# should raise an exception instead of returning a default.
+no_default = object()
+
+# Global namespace / alias registration map.  See
+# registerNamespaceAlias.
+registered_aliases = {}
+
+class NamespaceAliasRegistrationError(Exception):
+    """
+    Raised when an alias or namespace URI has already been registered.
+    """
+    pass
+
+def registerNamespaceAlias(namespace_uri, alias):
+    """
+    Registers a (namespace URI, alias) mapping in a global namespace
+    alias map.  Raises NamespaceAliasRegistrationError if either the
+    namespace URI or alias has already been registered with a
+    different value.  This function is required if you want to use a
+    namespace with an OpenID 1 message.
+    """
+    global registered_aliases
+
+    if registered_aliases.get(alias) == namespace_uri:
+        return
+
+    if namespace_uri in registered_aliases.values():
+        raise NamespaceAliasRegistrationError, \
+              'Namespace uri %r already registered' % (namespace_uri,)
+
+    if alias in registered_aliases:
+        raise NamespaceAliasRegistrationError, \
+              'Alias %r already registered' % (alias,)
+
+    registered_aliases[alias] = namespace_uri
+
+class Message(object):
+    """
+    In the implementation of this object, None represents the global
+    namespace as well as a namespace with no key.
+
+    @cvar namespaces: A dictionary specifying specific
+        namespace-URI to alias mappings that should be used when
+        generating namespace aliases.
+
+    @ivar ns_args: two-level dictionary of the values in this message,
+        grouped by namespace URI. The first level is the namespace
+        URI.
+    """
+
+    allowed_openid_namespaces = [OPENID1_NS, THE_OTHER_OPENID1_NS, OPENID2_NS]
+
+    def __init__(self, openid_namespace=None):
+        """Create an empty Message.
+
+        @raises InvalidOpenIDNamespace: if openid_namespace is not in
+            L{Message.allowed_openid_namespaces}
+        """
+        self.args = {}
+        self.namespaces = NamespaceMap()
+        if openid_namespace is None:
+            self._openid_ns_uri = None
+        else:
+            implicit = openid_namespace in OPENID1_NAMESPACES
+            self.setOpenIDNamespace(openid_namespace, implicit)
+
+    def fromPostArgs(cls, args):
+        """Construct a Message containing a set of POST arguments.
+
+        """
+        self = cls()
+
+        # Partition into "openid." args and bare args
+        openid_args = {}
+        for key, value in args.items():
+            if isinstance(value, list):
+                raise TypeError("query dict must have one value for each key, "
+                                "not lists of values.  Query is %r" % (args,))
+
+
+            try:
+                prefix, rest = key.split('.', 1)
+            except ValueError:
+                prefix = None
+
+            if prefix != 'openid':
+                self.args[(BARE_NS, key)] = value
+            else:
+                openid_args[rest] = value
+
+        self._fromOpenIDArgs(openid_args)
+
+        return self
+
+    fromPostArgs = classmethod(fromPostArgs)
+
+    def fromOpenIDArgs(cls, openid_args):
+        """Construct a Message from a parsed KVForm message.
+
+        @raises InvalidOpenIDNamespace: if openid.ns is not in
+            L{Message.allowed_openid_namespaces}
+        """
+        self = cls()
+        self._fromOpenIDArgs(openid_args)
+        return self
+
+    fromOpenIDArgs = classmethod(fromOpenIDArgs)
+
+    def _fromOpenIDArgs(self, openid_args):
+        ns_args = []
+
+        # Resolve namespaces
+        for rest, value in openid_args.iteritems():
+            try:
+                ns_alias, ns_key = rest.split('.', 1)
+            except ValueError:
+                ns_alias = NULL_NAMESPACE
+                ns_key = rest
+
+            if ns_alias == 'ns':
+                self.namespaces.addAlias(value, ns_key)
+            elif ns_alias == NULL_NAMESPACE and ns_key == 'ns':
+                # null namespace
+                self.setOpenIDNamespace(value, False)
+            else:
+                ns_args.append((ns_alias, ns_key, value))
+
+        # Implicitly set an OpenID namespace definition (OpenID 1)
+        if not self.getOpenIDNamespace():
+            self.setOpenIDNamespace(OPENID1_NS, True)
+
+        # Actually put the pairs into the appropriate namespaces
+        for (ns_alias, ns_key, value) in ns_args:
+            ns_uri = self.namespaces.getNamespaceURI(ns_alias)
+            if ns_uri is None:
+                # we found a namespaced arg without a namespace URI defined
+                ns_uri = self._getDefaultNamespace(ns_alias)
+                if ns_uri is None:
+                    ns_uri = self.getOpenIDNamespace()
+                    ns_key = '%s.%s' % (ns_alias, ns_key)
+                else:
+                    self.namespaces.addAlias(ns_uri, ns_alias, implicit=True)
+
+            self.setArg(ns_uri, ns_key, value)
+
+    def _getDefaultNamespace(self, mystery_alias):
+        """OpenID 1 compatibility: look for a default namespace URI to
+        use for this alias."""
+        global registered_aliases
+        # Only try to map an alias to a default if it's an
+        # OpenID 1.x message.
+        if self.isOpenID1():
+            return registered_aliases.get(mystery_alias)
+        else:
+            return None
+
+    def setOpenIDNamespace(self, openid_ns_uri, implicit):
+        """Set the OpenID namespace URI used in this message.
+
+        @raises InvalidOpenIDNamespace: if the namespace is not in
+            L{Message.allowed_openid_namespaces}
+        """
+        if openid_ns_uri not in self.allowed_openid_namespaces:
+            raise InvalidOpenIDNamespace(openid_ns_uri)
+
+        self.namespaces.addAlias(openid_ns_uri, NULL_NAMESPACE, implicit)
+        self._openid_ns_uri = openid_ns_uri
+
+    def getOpenIDNamespace(self):
+        return self._openid_ns_uri
+
+    def isOpenID1(self):
+        return self.getOpenIDNamespace() in OPENID1_NAMESPACES
+
+    def isOpenID2(self):
+        return self.getOpenIDNamespace() == OPENID2_NS
+
+    def fromKVForm(cls, kvform_string):
+        """Create a Message from a KVForm string"""
+        return cls.fromOpenIDArgs(kvform.kvToDict(kvform_string))
+
+    fromKVForm = classmethod(fromKVForm)
+
+    def copy(self):
+        return copy.deepcopy(self)
+
+    def toPostArgs(self):
+        """Return all arguments with openid. in front of namespaced arguments.
+        """
+        args = {}
+
+        # Add namespace definitions to the output
+        for ns_uri, alias in self.namespaces.iteritems():
+            if self.namespaces.isImplicit(ns_uri):
+                continue
+            if alias == NULL_NAMESPACE:
+                ns_key = 'openid.ns'
+            else:
+                ns_key = 'openid.ns.' + alias
+            args[ns_key] = ns_uri
+
+        for (ns_uri, ns_key), value in self.args.iteritems():
+            key = self.getKey(ns_uri, ns_key)
+            args[key] = value.encode('UTF-8')
+
+        return args
+
+    def toArgs(self):
+        """Return all namespaced arguments, failing if any
+        non-namespaced arguments exist."""
+        # FIXME - undocumented exception
+        post_args = self.toPostArgs()
+        kvargs = {}
+        for k, v in post_args.iteritems():
+            if not k.startswith('openid.'):
+                raise ValueError(
+                    'This message can only be encoded as a POST, because it '
+                    'contains arguments that are not prefixed with "openid."')
+            else:
+                kvargs[k[7:]] = v
+
+        return kvargs
+
+    def toFormMarkup(self, action_url, form_tag_attrs=None,
+                     submit_text="Continue"):
+        """Generate HTML form markup that contains the values in this
+        message, to be HTTP POSTed as x-www-form-urlencoded UTF-8.
+
+        @param action_url: The URL to which the form will be POSTed
+        @type action_url: str
+
+        @param form_tag_attrs: Dictionary of attributes to be added to
+            the form tag. 'accept-charset' and 'enctype' have defaults
+            that can be overridden. If a value is supplied for
+            'action' or 'method', it will be replaced.
+        @type form_tag_attrs: {unicode: unicode}
+
+        @param submit_text: The text that will appear on the submit
+            button for this form.
+        @type submit_text: unicode
+
+        @returns: A string containing (X)HTML markup for a form that
+            encodes the values in this Message object.
+        @rtype: str or unicode
+        """
+        if ElementTree is None:
+            raise RuntimeError('This function requires ElementTree.')
+
+        assert action_url is not None
+
+        form = ElementTree.Element('form')
+
+        if form_tag_attrs:
+            for name, attr in form_tag_attrs.iteritems():
+                form.attrib[name] = attr
+
+        form.attrib['action'] = action_url
+        form.attrib['method'] = 'post'
+        form.attrib['accept-charset'] = 'UTF-8'
+        form.attrib['enctype'] = 'application/x-www-form-urlencoded'
+
+        for name, value in self.toPostArgs().iteritems():
+            attrs = {'type': 'hidden',
+                     'name': name,
+                     'value': value}
+            form.append(ElementTree.Element('input', attrs))
+
+        submit = ElementTree.Element(
+                'input', {'type':'submit', 'value':submit_text})
+        form.append(submit)
+
+        return ElementTree.tostring(form)
+
+    def toURL(self, base_url):
+        """Generate a GET URL with the parameters in this message
+        attached as query parameters."""
+        return oidutil.appendArgs(base_url, self.toPostArgs())
+
+    def toKVForm(self):
+        """Generate a KVForm string that contains the parameters in
+        this message. This will fail if the message contains arguments
+        outside of the 'openid.' prefix.
+        """
+        return kvform.dictToKV(self.toArgs())
+
+    def toURLEncoded(self):
+        """Generate an x-www-urlencoded string"""
+        args = self.toPostArgs().items()
+        args.sort()
+        return urllib.urlencode(args)
+
+    def _fixNS(self, namespace):
+        """Convert an input value into the internally used values of
+        this object
+
+        @param namespace: The string or constant to convert
+        @type namespace: str or unicode or BARE_NS or OPENID_NS
+        """
+        if namespace == OPENID_NS:
+            if self._openid_ns_uri is None:
+                raise UndefinedOpenIDNamespace('OpenID namespace not set')
+            else:
+                namespace = self._openid_ns_uri
+
+        if namespace != BARE_NS and type(namespace) not in [str, unicode]:
+            raise TypeError(
+                "Namespace must be BARE_NS, OPENID_NS or a string. got %r"
+                % (namespace,))
+
+        if namespace != BARE_NS and ':' not in namespace:
+            fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r'
+            warnings.warn(fmt % (namespace,), DeprecationWarning)
+
+            if namespace == 'sreg':
+                fmt = 'Using %r instead of "sreg" as namespace'
+                warnings.warn(fmt % (SREG_URI,), DeprecationWarning,)
+                return SREG_URI
+
+        return namespace
+
+    def hasKey(self, namespace, ns_key):
+        namespace = self._fixNS(namespace)
+        return (namespace, ns_key) in self.args
+
+    def getKey(self, namespace, ns_key):
+        """Get the key for a particular namespaced argument"""
+        namespace = self._fixNS(namespace)
+        if namespace == BARE_NS:
+            return ns_key
+
+        ns_alias = self.namespaces.getAlias(namespace)
+
+        # No alias is defined, so no key can exist
+        if ns_alias is None:
+            return None
+
+        if ns_alias == NULL_NAMESPACE:
+            tail = ns_key
+        else:
+            tail = '%s.%s' % (ns_alias, ns_key)
+
+        return 'openid.' + tail
+
+    def getArg(self, namespace, key, default=None):
+        """Get a value for a namespaced key.
+
+        @param namespace: The namespace in the message for this key
+        @type namespace: str
+
+        @param key: The key to get within this namespace
+        @type key: str
+
+        @param default: The value to use if this key is absent from
+            this message. Using the special value
+            openid.message.no_default will result in this method
+            raising a KeyError instead of returning the default.
+
+        @rtype: str or the type of default
+        @raises KeyError: if default is no_default
+        @raises UndefinedOpenIDNamespace: if the message has not yet
+            had an OpenID namespace set
+        """
+        namespace = self._fixNS(namespace)
+        args_key = (namespace, key)
+        try:
+            return self.args[args_key]
+        except KeyError:
+            if default is no_default:
+                raise KeyError((namespace, key))
+            else:
+                return default
+
+    def getArgs(self, namespace):
+        """Get the arguments that are defined for this namespace URI
+
+        @returns: mapping from namespaced keys to values
+        @returntype: dict
+        """
+        namespace = self._fixNS(namespace)
+        return dict([
+            (ns_key, value)
+            for ((pair_ns, ns_key), value)
+            in self.args.iteritems()
+            if pair_ns == namespace
+            ])
+
+    def updateArgs(self, namespace, updates):
+        """Set multiple key/value pairs in one call
+
+        @param updates: The values to set
+        @type updates: {unicode:unicode}
+        """
+        namespace = self._fixNS(namespace)
+        for k, v in updates.iteritems():
+            self.setArg(namespace, k, v)
+
+    def setArg(self, namespace, key, value):
+        """Set a single argument in this namespace"""
+        assert key is not None
+        assert value is not None
+        namespace = self._fixNS(namespace)
+        self.args[(namespace, key)] = value
+        if not (namespace is BARE_NS):
+            self.namespaces.add(namespace)
+
+    def delArg(self, namespace, key):
+        namespace = self._fixNS(namespace)
+        del self.args[(namespace, key)]
+
+    def __repr__(self):
+        return "<%s.%s %r>" % (self.__class__.__module__,
+                               self.__class__.__name__,
+                               self.args)
+
+    def __eq__(self, other):
+        return self.args == other.args
+
+
+    def __ne__(self, other):
+        return not (self == other)
+
+
+    def getAliasedArg(self, aliased_key, default=None):
+        if aliased_key == 'ns':
+            return self.getOpenIDNamespace()
+
+        if aliased_key.startswith('ns.'):
+            uri = self.namespaces.getNamespaceURI(aliased_key[3:])
+            if uri is None:
+                if default == no_default:
+                    raise KeyError
+                else:
+                    return default
+            else:
+                return uri
+
+        try:
+            alias, key = aliased_key.split('.', 1)
+        except ValueError:
+            # need more than x values to unpack
+            ns = None
+        else:
+            ns = self.namespaces.getNamespaceURI(alias)
+
+        if ns is None:
+            key = aliased_key
+            ns = self.getOpenIDNamespace()
+
+        return self.getArg(ns, key, default)
+
+class NamespaceMap(object):
+    """Maintains a bijective map between namespace uris and aliases.
+    """
+    def __init__(self):
+        self.alias_to_namespace = {}
+        self.namespace_to_alias = {}
+        self.implicit_namespaces = []
+
+    def getAlias(self, namespace_uri):
+        return self.namespace_to_alias.get(namespace_uri)
+
+    def getNamespaceURI(self, alias):
+        return self.alias_to_namespace.get(alias)
+
+    def iterNamespaceURIs(self):
+        """Return an iterator over the namespace URIs"""
+        return iter(self.namespace_to_alias)
+
+    def iterAliases(self):
+        """Return an iterator over the aliases"""
+        return iter(self.alias_to_namespace)
+
+    def iteritems(self):
+        """Iterate over the mapping
+
+        @returns: iterator of (namespace_uri, alias)
+        """
+        return self.namespace_to_alias.iteritems()
+
+    def addAlias(self, namespace_uri, desired_alias, implicit=False):
+        """Add an alias from this namespace URI to the desired alias
+        """
+        # Check that desired_alias is not an openid protocol field as
+        # per the spec.
+        assert desired_alias not in OPENID_PROTOCOL_FIELDS, \
+               "%r is not an allowed namespace alias" % (desired_alias,)
+
+        # Check that desired_alias does not contain a period as per
+        # the spec.
+        if type(desired_alias) in [str, unicode]:
+            assert '.' not in desired_alias, \
+                   "%r must not contain a dot" % (desired_alias,)
+
+        # Check that there is not a namespace already defined for
+        # the desired alias
+        current_namespace_uri = self.alias_to_namespace.get(desired_alias)
+        if (current_namespace_uri is not None
+            and current_namespace_uri != namespace_uri):
+
+            fmt = ('Cannot map %r to alias %r. '
+                   '%r is already mapped to alias %r')
+
+            msg = fmt % (
+                namespace_uri,
+                desired_alias,
+                current_namespace_uri,
+                desired_alias)
+            raise KeyError(msg)
+
+        # Check that there is not already a (different) alias for
+        # this namespace URI
+        alias = self.namespace_to_alias.get(namespace_uri)
+        if alias is not None and alias != desired_alias:
+            fmt = ('Cannot map %r to alias %r. '
+                   'It is already mapped to alias %r')
+            raise KeyError(fmt % (namespace_uri, desired_alias, alias))
+
+        assert (desired_alias == NULL_NAMESPACE or
+                type(desired_alias) in [str, unicode]), repr(desired_alias)
+        assert namespace_uri not in self.implicit_namespaces
+        self.alias_to_namespace[desired_alias] = namespace_uri
+        self.namespace_to_alias[namespace_uri] = desired_alias
+        if implicit:
+            self.implicit_namespaces.append(namespace_uri)
+        return desired_alias
+
+    def add(self, namespace_uri):
+        """Add this namespace URI to the mapping, without caring what
+        alias it ends up with"""
+        # See if this namespace is already mapped to an alias
+        alias = self.namespace_to_alias.get(namespace_uri)
+        if alias is not None:
+            return alias
+
+        # Fall back to generating a numerical alias
+        i = 0
+        while True:
+            alias = 'ext' + str(i)
+            try:
+                self.addAlias(namespace_uri, alias)
+            except KeyError:
+                i += 1
+            else:
+                return alias
+
+        assert False, "Not reached"
+
+    def isDefined(self, namespace_uri):
+        return namespace_uri in self.namespace_to_alias
+
+    def __contains__(self, namespace_uri):
+        return self.isDefined(namespace_uri)
+
+    def isImplicit(self, namespace_uri):
+        return namespace_uri in self.implicit_namespaces

+ 190 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/oidutil.py

@@ -0,0 +1,190 @@
+"""This module contains general utility code that is used throughout
+the library.
+
+For users of this library, the C{L{log}} function is probably the most
+interesting.
+"""
+
+__all__ = ['log', 'appendArgs', 'toBase64', 'fromBase64', 'autoSubmitHTML']
+
+import binascii
+import sys
+import urlparse
+
+from urllib import urlencode
+
+elementtree_modules = [
+    'lxml.etree',
+    'xml.etree.cElementTree',
+    'xml.etree.ElementTree',
+    'cElementTree',
+    'elementtree.ElementTree',
+    ]
+
+def autoSubmitHTML(form, title='OpenID transaction in progress'):
+    return """
+<html>
+<head>
+  <title>%s</title>
+</head>
+<body onload="document.forms[0].submit();">
+%s
+<script>
+var elements = document.forms[0].elements;
+for (var i = 0; i < elements.length; i++) {
+  elements[i].style.display = "none";
+}
+</script>
+</body>
+</html>
+""" % (title, form)
+
+def importElementTree(module_names=None):
+    """Find a working ElementTree implementation, trying the standard
+    places that such a thing might show up.
+
+    >>> ElementTree = importElementTree()
+
+    @param module_names: The names of modules to try to use as
+        ElementTree. Defaults to C{L{elementtree_modules}}
+
+    @returns: An ElementTree module
+    """
+    if module_names is None:
+        module_names = elementtree_modules
+
+    for mod_name in module_names:
+        try:
+            ElementTree = __import__(mod_name, None, None, ['unused'])
+        except ImportError:
+            pass
+        else:
+            # Make sure it can actually parse XML
+            try:
+                ElementTree.XML('<unused/>')
+            except (SystemExit, MemoryError, AssertionError):
+                raise
+            except:
+                why = sys.exc_info()[1]
+                log('Not using ElementTree library %r because it failed to '
+                    'parse a trivial document: %s' % (mod_name, why))
+            else:
+                return ElementTree
+    else:
+        raise ImportError('No ElementTree library found. '
+                          'You may need to install one. '
+                          'Tried importing %r' % (module_names,)
+                          )
+
+def log(message, level=0):
+    """Handle a log message from the OpenID library.
+
+    This implementation writes the string it to C{sys.stderr},
+    followed by a newline.
+
+    Currently, the library does not use the second parameter to this
+    function, but that may change in the future.
+
+    To install your own logging hook::
+
+      from openid import oidutil
+
+      def myLoggingFunction(message, level):
+          ...
+
+      oidutil.log = myLoggingFunction
+
+    @param message: A string containing a debugging message from the
+        OpenID library
+    @type message: str
+
+    @param level: The severity of the log message. This parameter is
+        currently unused, but in the future, the library may indicate
+        more important information with a higher level value.
+    @type level: int or None
+
+    @returns: Nothing.
+    """
+
+    sys.stderr.write(message)
+    sys.stderr.write('\n')
+
+def appendArgs(url, args):
+    """Append query arguments to a HTTP(s) URL. If the URL already has
+    query arguemtns, these arguments will be added, and the existing
+    arguments will be preserved. Duplicate arguments will not be
+    detected or collapsed (both will appear in the output).
+
+    @param url: The url to which the arguments will be appended
+    @type url: str
+
+    @param args: The query arguments to add to the URL. If a
+        dictionary is passed, the items will be sorted before
+        appending them to the URL. If a sequence of pairs is passed,
+        the order of the sequence will be preserved.
+    @type args: A dictionary from string to string, or a sequence of
+        pairs of strings.
+
+    @returns: The URL with the parameters added
+    @rtype: str
+    """
+    if hasattr(args, 'items'):
+        args = args.items()
+        args.sort()
+    else:
+        args = list(args)
+
+    if len(args) == 0:
+        return url
+
+    if '?' in url:
+        sep = '&'
+    else:
+        sep = '?'
+
+    # Map unicode to UTF-8 if present. Do not make any assumptions
+    # about the encodings of plain bytes (str).
+    i = 0
+    for k, v in args:
+        if type(k) is not str:
+            k = k.encode('UTF-8')
+
+        if type(v) is not str:
+            v = v.encode('UTF-8')
+
+        args[i] = (k, v)
+        i += 1
+
+    return '%s%s%s' % (url, sep, urlencode(args))
+
+def toBase64(s):
+    """Represent string s as base64, omitting newlines"""
+    return binascii.b2a_base64(s)[:-1]
+
+def fromBase64(s):
+    try:
+        return binascii.a2b_base64(s)
+    except binascii.Error, why:
+        # Convert to a common exception type
+        raise ValueError(why[0])
+
+class Symbol(object):
+    """This class implements an object that compares equal to others
+    of the same type that have the same name. These are distict from
+    str or unicode objects.
+    """
+
+    def __init__(self, name):
+        self.name = name
+
+    def __eq__(self, other):
+        return type(self) is type(other) and self.name == other.name
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __hash__(self):
+        return hash((self.__class__, self.name))
+   
+    def __repr__(self):
+        return '<Symbol %s>' % (self.name,)

+ 6 - 0
desktop/core/ext-py/python-openid-2.2.5/openid/server/__init__.py

@@ -0,0 +1,6 @@
+"""
+This package contains the portions of the library used only when
+implementing an OpenID server.  See L{openid.server.server}.
+"""
+
+__all__ = ['server', 'trustroot']

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů