launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01128
[Merge] lp:~leonardr/launchpad/revert-oauth-aware-website into lp:launchpad/devel
Leonard Richardson has proposed merging lp:~leonardr/launchpad/revert-oauth-aware-website into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
This branch reverts my recent branch to make parts of the Launchpad website accept OAuth-signed requests. I'm reverting it not because the code is bad, but because the requirements changed immediately after I merged this branch, rendering it moot.
--
https://code.launchpad.net/~leonardr/launchpad/revert-oauth-aware-website/+merge/36040
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~leonardr/launchpad/revert-oauth-aware-website into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/browser/oauth.py'
--- lib/canonical/launchpad/browser/oauth.py 2010-09-15 20:06:13 +0000
+++ lib/canonical/launchpad/browser/oauth.py 2010-09-20 17:48:06 +0000
@@ -11,13 +11,11 @@
from lazr.restful import HTTPResource
import simplejson
-from zope.authentication.interfaces import IUnauthenticatedPrincipal
from zope.component import getUtility
from zope.formlib.form import (
Action,
Actions,
)
-from zope.security.interfaces import Unauthorized
from canonical.launchpad.interfaces.oauth import (
IOAuthConsumerSet,
@@ -31,15 +29,9 @@
)
from canonical.launchpad.webapp.authentication import (
check_oauth_signature,
- extract_oauth_access_token,
get_oauth_authorization,
- get_oauth_principal
- )
-from canonical.launchpad.webapp.interfaces import (
- AccessLevel,
- ILaunchBag,
- OAuthPermission,
- )
+ )
+from canonical.launchpad.webapp.interfaces import OAuthPermission
from lp.app.errors import UnexpectedFormData
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.pillar import IPillarNameSet
@@ -106,7 +98,6 @@
return u'oauth_token=%s&oauth_token_secret=%s' % (
token.key, token.secret)
-
def token_exists_and_is_not_reviewed(form, action):
return form.token is not None and not form.token.is_reviewed
@@ -115,10 +106,8 @@
"""Return a list of `Action`s for each possible `OAuthPermission`."""
actions = Actions()
actions_excluding_grant_permissions = Actions()
-
def success(form, action, data):
form.reviewToken(action.permission)
-
for permission in OAuthPermission.items:
action = Action(
permission.title, name=permission.name, success=success,
@@ -129,86 +118,7 @@
actions_excluding_grant_permissions.append(action)
return actions, actions_excluding_grant_permissions
-
-class CredentialManagerAwareMixin:
- """A view for which a browser may authenticate with an OAuth token.
-
- The OAuth token must be signed with a token that has the
- GRANT_PERMISSIONS access level, and the browser must present
- itself as the Launchpad Credentials Manager.
- """
- # A prefix identifying the Launchpad Credential Manager's
- # User-Agent string.
- GRANT_PERMISSIONS_USER_AGENT_PREFIX = "Launchpad Credentials Manager"
-
- def ensureRequestIsAuthorizedOrSigned(self):
- """Find the user who initiated the request.
-
- This property is used by a view that wants to reject access
- unless the end-user is authenticated with cookie auth, HTTP
- Basic Auth, *or* a properly authorized OAuth token.
-
- If the user is logged in with cookie auth or HTTP Basic, then
- other parts of Launchpad have taken care of the login and we
- don't have to do anything. But if the user's browser has
- signed the request with an OAuth token, other parts of
- Launchpad won't recognize that as an attempt to authorize the
- request.
-
- This method does the OAuth part of the work. It checks that
- the OAuth token is valid, that it's got the correct access
- level, and that the User-Agent is one that's allowed to sign
- requests with OAuth tokens.
-
- :return: The user who Launchpad identifies as the principal.
- Or, if Launchpad identifies no one as the principal, the user
- whose valid GRANT_PERMISSIONS OAuth token was used to sign
- the request.
-
- :raise Unauthorized: If the request is unauthorized and
- unsigned, improperly signed, anonymously signed, or signed
- with a token that does not have the right access level.
- """
- user = getUtility(ILaunchBag).user
- if user is not None:
- return user
- # The normal Launchpad code was not able to identify any
- # user, but we're going to try a little harder before
- # concluding that no one's logged in. If the incoming
- # request is signed by an OAuth access token with the
- # GRANT_PERMISSIONS access level, we will force a
- # temporary login with the user whose access token this
- # is.
- token = extract_oauth_access_token(self.request)
- if token is None:
- # The request is not OAuth-signed. The normal Launchpad
- # code had it right: no one is authenticated.
- raise Unauthorized("Anonymous access is not allowed.")
- principal = get_oauth_principal(self.request)
- if IUnauthenticatedPrincipal.providedBy(principal):
- # The request is OAuth-signed, but as the anonymous
- # user.
- raise Unauthorized("Anonymous access is not allowed.")
- if token.permission != AccessLevel.GRANT_PERMISSIONS:
- # The request is OAuth-signed, but the token has
- # the wrong access level.
- raise Unauthorized("OAuth token has insufficient access level.")
-
- # Both the consumer key and the User-Agent must identify the
- # Launchpad Credentials Manager.
- must_start_with_prefix = [
- token.consumer.key, self.request.getHeader("User-Agent")]
- for string in must_start_with_prefix:
- if not string.startswith(
- self.GRANT_PERMISSIONS_USER_AGENT_PREFIX):
- raise Unauthorized(
- "Only the Launchpad Credentials Manager can access this "
- "page by signing requests with an OAuth token.")
- return principal.person
-
-
-class OAuthAuthorizeTokenView(
- LaunchpadFormView, JSONTokenMixin, CredentialManagerAwareMixin):
+class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
"""Where users authorize consumers to access Launchpad on their behalf."""
actions, actions_excluding_grant_permissions = (
@@ -257,12 +167,6 @@
and len(allowed_permissions) > 1):
allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name)
- # GRANT_PERMISSIONS may only be requested by a specific User-Agent.
- if (OAuthPermission.GRANT_PERMISSIONS.name in allowed_permissions
- and not self.request.getHeader("User-Agent").startswith(
- self.GRANT_PERMISSIONS_USER_AGENT_PREFIX)):
- allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name)
-
for action in self.actions:
if (action.permission.name in allowed_permissions
or action.permission is OAuthPermission.UNAUTHORIZED):
@@ -280,10 +184,9 @@
return actions
def initialize(self):
- self.oauth_authorized_user = self.ensureRequestIsAuthorizedOrSigned()
self.storeTokenContext()
-
- key = self.request.form.get('oauth_token')
+ form = get_oauth_authorization(self.request)
+ key = form.get('oauth_token')
if key:
self.token = getUtility(IOAuthRequestTokenSet).getByKey(key)
super(OAuthAuthorizeTokenView, self).initialize()
@@ -314,8 +217,7 @@
self.token_context = context
def reviewToken(self, permission):
- self.token.review(self.user or self.oauth_authorized_user,
- permission, self.token_context)
+ self.token.review(self.user, permission, self.token_context)
callback = self.request.form.get('oauth_callback')
if callback:
self.next_url = callback
@@ -343,7 +245,7 @@
return context
-class OAuthTokenAuthorizedView(LaunchpadView, CredentialManagerAwareMixin):
+class OAuthTokenAuthorizedView(LaunchpadView):
"""Where users who reviewed tokens may get redirected to.
If the consumer didn't include an oauth_callback when sending the user to
@@ -352,7 +254,6 @@
"""
def initialize(self):
- authorized_user = self.ensureRequestIsAuthorizedOrSigned()
key = self.request.form.get('oauth_token')
self.token = getUtility(IOAuthRequestTokenSet).getByKey(key)
assert self.token.is_reviewed, (
=== modified file 'lib/canonical/launchpad/database/oauth.py'
--- lib/canonical/launchpad/database/oauth.py 2010-09-15 20:55:03 +0000
+++ lib/canonical/launchpad/database/oauth.py 2010-09-20 17:48:06 +0000
@@ -60,14 +60,14 @@
# How many hours should a request token be valid for?
REQUEST_TOKEN_VALIDITY = 12
-# The OAuth Core 1.0 spec (http://oauth.net/core/1.0/#nonce) says that
-# a timestamp "MUST be equal or greater than the timestamp used in
-# previous requests," but this is likely to cause problems if the
-# client does request pipelining, so we use a time window (relative to
-# the timestamp of the existing OAuthNonce) to check if the timestamp
-# can is acceptable. As suggested by Robert, we use a window which is
-# at least twice the size of our hard time out. This is a safe bet
-# since no requests should take more than one hard time out.
+# The OAuth Core 1.0 spec (http://oauth.net/core/1.0/#nonce) says that a
+# timestamp "MUST be equal or greater than the timestamp used in previous
+# requests," but this is likely to cause problems if the client does request
+# pipelining, so we use a time window (relative to the timestamp of the
+# existing OAuthNonce) to check if the timestamp can is acceptable. As
+# suggested by Robert, we use a window which is at least twice the size of our
+# hard time out. This is a safe bet since no requests should take more than
+# one hard time out.
TIMESTAMP_ACCEPTANCE_WINDOW = 60 # seconds
# If the timestamp is far in the future because of a client's clock skew,
# it will effectively invalidate the authentication tokens when the clock is
@@ -77,7 +77,6 @@
# amount.
TIMESTAMP_SKEW_WINDOW = 60*60 # seconds, +/-
-
class OAuthBase(SQLBase):
"""Base class for all OAuth database classes."""
@@ -94,7 +93,6 @@
getStore = _get_store
-
class OAuthConsumer(OAuthBase):
"""See `IOAuthConsumer`."""
implements(IOAuthConsumer)
@@ -325,7 +323,6 @@
The key will have a length of 20 and we'll make sure it's not yet in the
given table. The secret will have a length of 80.
"""
-
key_length = 20
key = create_unique_token_for_table(key_length, getattr(table, "key"))
secret_length = 80
=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-09-16 21:34:31 +0000
+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-09-20 17:48:06 +0000
@@ -1,6 +1,4 @@
-***************************
-Authorizing a request token
-***************************
+= Authorizing a request token =
Once the consumer gets a request token, it must send the user to
Launchpad's +authorize-token page in order for the user to authenticate
@@ -21,10 +19,9 @@
The oauth_token parameter, on the other hand, is required in the
Launchpad implementation.
-Access to the page
-==================
-
-The +authorize-token page is restricted to authenticated users.
+The +authorize-token page is restricted to logged in users, so users will
+first be asked to log in. (We won't show the actual login process because
+it involves OpenID, which would complicate this test quite a bit.)
>>> from urllib import urlencode
>>> params = dict(
@@ -33,18 +30,7 @@
>>> browser.open(url)
Traceback (most recent call last):
...
- Unauthorized: Anonymous access is not allowed.
-
-However, the details of the authentication are different than from any
-other part of Launchpad. Unlike with other pages, a user can authorize
-an OAuth token by signing their outgoing requests with an _existing_
-OAuth token. This makes it possible for a desktop client to retrieve
-this page without knowing the end-user's username and password, or
-making them navigate the arbitrarily complex OpenID login procedure.
-
-But, let's deal with that a little later. First let's show how the
-process works through HTTP Basic Auth (the testing equivalent of a
-regular username-and-password login).
+ Unauthorized:...
>>> browser = setupBrowser(auth='Basic no-priv@xxxxxxxxxxxxx:test')
>>> browser.open(url)
@@ -58,10 +44,6 @@
...
See all applications authorized to access Launchpad on your behalf.
-
-Using the page
-==============
-
This page contains one submit button for each item of OAuthPermission,
except for 'Grant Permissions', which must be specifically requested.
@@ -92,34 +74,7 @@
that isn't enough for the application. The user always has the option
to deny permission altogether.
- >>> def filter_user_agent(key, value, new_value):
- ... """A filter to replace the User-Agent header in a list of headers.
- ...
- ... [XXX bug=638058] This is a hack to work around a bug in
- ... zope.testbrowser.
- ... """
- ...
- ... if key.lower() == "user-agent":
- ... return (key, new_value)
- ... return (key, value)
-
- >>> def print_access_levels(allow_permission, user_agent=None):
- ... if user_agent is not None:
- ... # [XXX bug=638058] This is a hack to work around a bug in
- ... # zope.testbrowser which prevents browser.addHeader
- ... # from working with User-Agent.
- ... mech_browser = browser.mech_browser
- ... # Store the original User-Agent for later.
- ... old_user_agent = [
- ... value for key, value in mech_browser.addheaders
- ... if key.lower() == "user-agent"][0]
- ... # Replace the User-Agent with the value passed into this
- ... # function.
- ... mech_browser.addheaders = [
- ... filter_user_agent(key, value, user_agent)
- ... for key, value in mech_browser.addheaders]
- ...
- ... # Okay, now we can make the request.
+ >>> def print_access_levels(allow_permission):
... browser.open(
... "http://launchpad.dev/+authorize-token?%s&%s"
... % (urlencode(params), allow_permission))
@@ -127,13 +82,6 @@
... actions = main_content.findAll('input', attrs={'type': 'submit'})
... for action in actions:
... print action['value']
- ...
- ... if user_agent is not None:
- ... # Finally, restore the old User-Agent.
- ... mech_browser.addheaders = [
- ... filter_user_agent(key, value, old_user_agent)
- ... for key, value in mech_browser.addheaders]
-
>>> print_access_levels(
... 'allow_permission=WRITE_PUBLIC&allow_permission=WRITE_PRIVATE')
@@ -142,38 +90,23 @@
Change Anything
The only time the 'Grant Permissions' permission shows up in this list
-is if a client identifying itself as the Launchpad Credentials Manager
-specifically requests it, and no other permission. (Also requesting
-UNAUTHORIZED is okay--it will show up anyway.)
-
- >>> USER_AGENT = "Launchpad Credentials Manager v1.0"
- >>> print_access_levels(
- ... 'allow_permission=GRANT_PERMISSIONS', USER_AGENT)
- No Access
- Grant Permissions
-
- >>> print_access_levels(
- ... ('allow_permission=GRANT_PERMISSIONS&'
- ... 'allow_permission=UNAUTHORIZED'),
- ... USER_AGENT)
- No Access
- Grant Permissions
-
- >>> print_access_levels(
- ... ('allow_permission=WRITE_PUBLIC&'
- ... 'allow_permission=GRANT_PERMISSIONS'))
- No Access
- Change Non-Private Data
-
-If a client asks for GRANT_PERMISSIONS but doesn't claim to be the
-Launchpad Credentials Manager, Launchpad will not show GRANT_PERMISSIONS.
+is if the client specifically requests it, and no other
+permission. (Also requesting UNAUTHORIZED is okay--it will show up
+anyway.)
>>> print_access_levels('allow_permission=GRANT_PERMISSIONS')
No Access
- Read Non-Private Data
+ Grant Permissions
+
+ >>> print_access_levels(
+ ... 'allow_permission=GRANT_PERMISSIONS&allow_permission=UNAUTHORIZED')
+ No Access
+ Grant Permissions
+
+ >>> print_access_levels(
+ ... 'allow_permission=WRITE_PUBLIC&allow_permission=GRANT_PERMISSIONS')
+ No Access
Change Non-Private Data
- Read Anything
- Change Anything
If an application doesn't specify any valid access levels, or only
specifies the UNAUTHORIZED access level, Launchpad will show all the
@@ -330,124 +263,3 @@
This request for accessing Launchpad on your behalf has been
reviewed ... ago.
See all applications authorized to access Launchpad on your behalf.
-
-Access through OAuth
-====================
-
-Now it's time to show how to go through the same process without
-knowing the end-user's username and password. All you need is an OAuth
-token issued with the GRANT_PERMISSIONS access level, in the name of
-the Launchpad Credentials Manager.
-
-Let's go through the approval process again, without ever sending the
-user's username or password over HTTP. First we'll create a new user,
-and a GRANT_PERMISSIONS access token that they can use to sign
-requests.
-
- >>> login(ANONYMOUS)
- >>> user = factory.makePerson(name="test-user", password="never-used")
- >>> logout()
-
- >>> from oauth.oauth import OAuthConsumer
- >>> manager_consumer = OAuthConsumer("Launchpad Credentials Manager", "")
-
- >>> from lp.testing import oauth_access_token_for
- >>> login_person(user)
- >>> grant_permissions_token = oauth_access_token_for(
- ... manager_consumer.key, user, "GRANT_PERMISSIONS")
- >>> logout()
-
-Next, we'll give the new user an OAuth request token that needs to be
-approved using a web browser.
-
- >>> login_person(user)
- >>> consumer = getUtility(IOAuthConsumerSet).getByKey('foobar123451432')
- >>> request_token = consumer.newRequestToken()
- >>> logout()
-
- >>> params = dict(oauth_token=request_token.key)
- >>> url = "http://launchpad.dev/+authorize-token?%s" % urlencode(params)
-
-Next, we'll create a browser object that knows how to sign requests
-with the new user's existing access token.
-
- >>> from lp.testing import OAuthSigningBrowser
- >>> browser = OAuthSigningBrowser(
- ... manager_consumer, grant_permissions_token, USER_AGENT)
- >>> browser.open(url)
- >>> print browser.title
- Authorize application to access Launchpad on your behalf
-
-The browser object can approve the request and see the appropriate
-messages, even though we never gave it the user's password.
-
- >>> browser.getControl('Read Anything').click()
-
- >>> browser.url
- 'http://launchpad.dev/+token-authorized?...'
- >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
- Almost finished ...
- To finish authorizing the application identified as foobar123451432 to
- access Launchpad on your behalf you should go back to the application
- window in which you started the process and inform it that you have done
- your part of the process.
-
-OAuth error conditions
-----------------------
-
-The OAuth token used to sign the requests must have the
-GRANT_PERMISSIONS access level; no other access level will work.
-
- >>> login(ANONYMOUS)
- >>> insufficient_token = oauth_access_token_for(
- ... manager_consumer.key, user, "WRITE_PRIVATE")
- >>> logout()
-
- >>> browser = OAuthSigningBrowser(
- ... manager_consumer, insufficient_token, USER_AGENT)
- >>> browser.open(url)
- Traceback (most recent call last):
- ...
- Unauthorized: OAuth token has insufficient access level.
-
-The OAuth token must be for the Launchpad Credentials Manager, or it
-cannot be used. (Launchpad shouldn't even _issue_ a GRANT_PERMISSIONS
-token for any other consumer, but even if it somehow does, that token
-can't be used for this.)
-
- >>> login(ANONYMOUS)
- >>> wrong_consumer = OAuthConsumer(
- ... "Not the Launchpad Credentials Manager", "")
- >>> wrong_consumer_token = oauth_access_token_for(
- ... wrong_consumer.key, user, "GRANT_PERMISSIONS")
- >>> logout()
-
- >>> browser = OAuthSigningBrowser(wrong_consumer, wrong_consumer_token)
- >>> browser.open(url)
- Traceback (most recent call last):
- ...
- Unauthorized: Only the Launchpad Credentials Manager can access
- this page by signing requests with an OAuth token.
-
-Signing with an anonymous token will also not work.
-
- >>> from oauth.oauth import OAuthToken
- >>> anonymous_token = OAuthToken(key="", secret="")
- >>> browser = OAuthSigningBrowser(manager_consumer, anonymous_token)
- >>> browser.open(url)
- Traceback (most recent call last):
- ...
- Unauthorized: Anonymous access is not allowed.
-
-Even if it presents the right token, the user agent sending the signed
-request must *also* identify *itself* as the Launchpad Credentials
-Manager.
-
- >>> browser = OAuthSigningBrowser(
- ... manager_consumer, grant_permissions_token,
- ... "Not the Launchpad Credentials Manager")
- >>> browser.open(url)
- Traceback (most recent call last):
- ...
- Unauthorized: Only the Launchpad Credentials Manager can access
- this page by signing requests with an OAuth token.
=== modified file 'lib/canonical/launchpad/webapp/authentication.py'
--- lib/canonical/launchpad/webapp/authentication.py 2010-09-16 15:40:56 +0000
+++ lib/canonical/launchpad/webapp/authentication.py 2010-09-20 17:48:06 +0000
@@ -5,21 +5,16 @@
__all__ = [
'check_oauth_signature',
- 'extract_oauth_access_token',
- 'get_oauth_principal',
'get_oauth_authorization',
'LaunchpadLoginSource',
'LaunchpadPrincipal',
- 'OAuthSignedRequest',
'PlacelessAuthUtility',
'SSHADigestEncryptor',
]
import binascii
-from datetime import datetime
import hashlib
-import pytz
import random
from UserDict import UserDict
@@ -28,18 +23,13 @@
from zope.app.security.interfaces import ILoginPassword
from zope.app.security.principalregistry import UnauthenticatedPrincipal
from zope.authentication.interfaces import IUnauthenticatedPrincipal
-
from zope.component import (
adapts,
getUtility,
)
from zope.event import notify
-from zope.interface import (
- alsoProvides,
- implements,
- )
+from zope.interface import implements
from zope.preference.interfaces import IPreferenceGroup
-from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from zope.session.interfaces import ISession
@@ -54,14 +44,6 @@
ILaunchpadPrincipal,
IPlacelessAuthUtility,
IPlacelessLoginSource,
- OAuthPermission,
- )
-from canonical.launchpad.interfaces.oauth import (
- ClockSkew,
- IOAuthConsumerSet,
- IOAuthSignedRequest,
- NonceAlreadyUsed,
- TimestampOrderingError,
)
from lp.registry.interfaces.person import (
IPerson,
@@ -69,113 +51,6 @@
)
-def extract_oauth_access_token(request):
- """Find the OAuth access token that signed the given request.
-
- :param request: An incoming request.
-
- :return: an IOAuthAccessToken, or None if the request is not
- signed at all.
-
- :raise Unauthorized: If the token is invalid or the request is an
- anonymously-signed request that doesn't meet our requirements.
- """
- # Fetch OAuth authorization information from the request.
- form = get_oauth_authorization(request)
-
- consumer_key = form.get('oauth_consumer_key')
- consumers = getUtility(IOAuthConsumerSet)
- consumer = consumers.getByKey(consumer_key)
- token_key = form.get('oauth_token')
- anonymous_request = (token_key == '')
-
- if consumer_key is None:
- # Either the client's OAuth implementation is broken, or
- # the user is trying to make an unauthenticated request
- # using wget or another OAuth-ignorant application.
- # Try to retrieve a consumer based on the User-Agent
- # header.
- anonymous_request = True
- consumer_key = request.getHeader('User-Agent', '')
- if consumer_key == '':
- raise Unauthorized(
- 'Anonymous requests must provide a User-Agent.')
- consumer = consumers.getByKey(consumer_key)
-
- if consumer is None:
- if anonymous_request:
- # This is the first time anyone has tried to make an
- # anonymous request using this consumer name (or user
- # agent). Dynamically create the consumer.
- #
- # In the normal website this wouldn't be possible
- # because GET requests have their transactions rolled
- # back. But webservice requests always have their
- # transactions committed so that we can keep track of
- # the OAuth nonces and prevent replay attacks.
- if consumer_key == '' or consumer_key is None:
- raise Unauthorized("No consumer key specified.")
- consumer = consumers.new(consumer_key, '')
- else:
- # An unknown consumer can never make a non-anonymous
- # request, because access tokens are registered with a
- # specific, known consumer.
- raise Unauthorized('Unknown consumer (%s).' % consumer_key)
- if anonymous_request:
- # Skip the OAuth verification step and let the user access the
- # web service as an unauthenticated user.
- #
- # XXX leonardr 2009-12-15 bug=496964: Ideally we'd be
- # auto-creating a token for the anonymous user the first
- # time, passing it through the OAuth verification step,
- # and using it on all subsequent anonymous requests.
- return None
-
- token = consumer.getAccessToken(token_key)
- if token is None:
- raise Unauthorized('Unknown access token (%s).' % token_key)
- return token
-
-
-def get_oauth_principal(request):
- """Find the principal to use for this OAuth-signed request.
-
- :param request: An incoming request.
- :return: An ILaunchpadPrincipal with the appropriate access level.
- """
- token = extract_oauth_access_token(request)
-
- if token is None:
- # The consumer is making an anonymous request. If there was a
- # problem with the access token, extract_oauth_access_token
- # would have raised Unauthorized.
- alsoProvides(request, IOAuthSignedRequest)
- auth_utility = getUtility(IPlacelessAuthUtility)
- return auth_utility.unauthenticatedPrincipal()
-
- form = get_oauth_authorization(request)
- nonce = form.get('oauth_nonce')
- timestamp = form.get('oauth_timestamp')
- try:
- token.checkNonceAndTimestamp(nonce, timestamp)
- except (NonceAlreadyUsed, TimestampOrderingError, ClockSkew), e:
- raise Unauthorized('Invalid nonce/timestamp: %s' % e)
- now = datetime.now(pytz.timezone('UTC'))
- if token.permission == OAuthPermission.UNAUTHORIZED:
- raise Unauthorized('Unauthorized token (%s).' % token.key)
- elif token.date_expires is not None and token.date_expires <= now:
- raise Unauthorized('Expired token (%s).' % token.key)
- elif not check_oauth_signature(request, token.consumer, token):
- raise Unauthorized('Invalid signature.')
- else:
- # Everything is fine, let's return the principal.
- pass
- alsoProvides(request, IOAuthSignedRequest)
- return getUtility(IPlacelessLoginSource).getPrincipal(
- token.person.account.id, access_level=token.permission,
- scope=token.context)
-
-
class PlacelessAuthUtility:
"""An authentication service which holds no state aside from its
ZCML configuration, implemented as a utility.
@@ -200,8 +75,9 @@
# as the login form is never visited for BasicAuth.
# This we treat each request as a separate
# login/logout.
- notify(
- BasicAuthLoggedInEvent(request, login, principal))
+ notify(BasicAuthLoggedInEvent(
+ request, login, principal
+ ))
return principal
def _authenticateUsingCookieAuth(self, request):
@@ -314,8 +190,7 @@
plaintext = str(plaintext)
if salt is None:
salt = self.generate_salt()
- v = binascii.b2a_base64(
- hashlib.sha1(plaintext + salt).digest() + salt)
+ v = binascii.b2a_base64(hashlib.sha1(plaintext + salt).digest() + salt)
return v[:-1]
def validate(self, plaintext, encrypted):
@@ -459,7 +334,6 @@
# zope.app.apidoc expects our principals to be adaptable into IAnnotations, so
# we use these dummy adapters here just to make that code not OOPS.
-
class TemporaryPrincipalAnnotations(UserDict):
implements(IAnnotations)
adapts(ILaunchpadPrincipal, IPreferenceGroup)
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2010-09-16 21:08:51 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2010-09-20 17:48:06 +0000
@@ -8,6 +8,7 @@
__metaclass__ = type
import cgi
+from datetime import datetime
import threading
import xmlrpclib
@@ -21,6 +22,7 @@
WebServiceRequestTraversal,
)
from lazr.uri import URI
+import pytz
import transaction
from transaction.interfaces import ISynchronizer
from zc.zservertracelog.tracelog import Server as ZServerTracelogServer
@@ -48,7 +50,10 @@
XMLRPCRequest,
XMLRPCResponse,
)
-from zope.security.interfaces import IParticipation
+from zope.security.interfaces import (
+ IParticipation,
+ Unauthorized,
+ )
from zope.security.proxy import (
isinstance as zope_isinstance,
removeSecurityProxy,
@@ -63,9 +68,17 @@
IPrivateApplication,
IWebServiceApplication,
)
+from canonical.launchpad.interfaces.oauth import (
+ ClockSkew,
+ IOAuthConsumerSet,
+ IOAuthSignedRequest,
+ NonceAlreadyUsed,
+ TimestampOrderingError,
+ )
import canonical.launchpad.layers
from canonical.launchpad.webapp.authentication import (
- get_oauth_principal,
+ check_oauth_signature,
+ get_oauth_authorization,
)
from canonical.launchpad.webapp.authorization import (
LAUNCHPAD_SECURITY_POLICY_CACHE_KEY,
@@ -80,6 +93,8 @@
INotificationRequest,
INotificationResponse,
IPlacelessAuthUtility,
+ IPlacelessLoginSource,
+ OAuthPermission,
)
from canonical.launchpad.webapp.notifications import (
NotificationList,
@@ -1201,7 +1216,83 @@
if request_path.startswith("/%s" % web_service_config.path_override):
return super(WebServicePublication, self).getPrincipal(request)
- return get_oauth_principal(request)
+ # Fetch OAuth authorization information from the request.
+ form = get_oauth_authorization(request)
+
+ consumer_key = form.get('oauth_consumer_key')
+ consumers = getUtility(IOAuthConsumerSet)
+ consumer = consumers.getByKey(consumer_key)
+ token_key = form.get('oauth_token')
+ anonymous_request = (token_key == '')
+
+ if consumer_key is None:
+ # Either the client's OAuth implementation is broken, or
+ # the user is trying to make an unauthenticated request
+ # using wget or another OAuth-ignorant application.
+ # Try to retrieve a consumer based on the User-Agent
+ # header.
+ anonymous_request = True
+ consumer_key = request.getHeader('User-Agent', '')
+ if consumer_key == '':
+ raise Unauthorized(
+ 'Anonymous requests must provide a User-Agent.')
+ consumer = consumers.getByKey(consumer_key)
+
+ if consumer is None:
+ if anonymous_request:
+ # This is the first time anyone has tried to make an
+ # anonymous request using this consumer name (or user
+ # agent). Dynamically create the consumer.
+ #
+ # In the normal website this wouldn't be possible
+ # because GET requests have their transactions rolled
+ # back. But webservice requests always have their
+ # transactions committed so that we can keep track of
+ # the OAuth nonces and prevent replay attacks.
+ if consumer_key == '' or consumer_key is None:
+ raise Unauthorized("No consumer key specified.")
+ consumer = consumers.new(consumer_key, '')
+ else:
+ # An unknown consumer can never make a non-anonymous
+ # request, because access tokens are registered with a
+ # specific, known consumer.
+ raise Unauthorized('Unknown consumer (%s).' % consumer_key)
+ if anonymous_request:
+ # Skip the OAuth verification step and let the user access the
+ # web service as an unauthenticated user.
+ #
+ # XXX leonardr 2009-12-15 bug=496964: Ideally we'd be
+ # auto-creating a token for the anonymous user the first
+ # time, passing it through the OAuth verification step,
+ # and using it on all subsequent anonymous requests.
+ alsoProvides(request, IOAuthSignedRequest)
+ auth_utility = getUtility(IPlacelessAuthUtility)
+ return auth_utility.unauthenticatedPrincipal()
+ token = consumer.getAccessToken(token_key)
+ if token is None:
+ raise Unauthorized('Unknown access token (%s).' % token_key)
+ nonce = form.get('oauth_nonce')
+ timestamp = form.get('oauth_timestamp')
+ try:
+ token.checkNonceAndTimestamp(nonce, timestamp)
+ except (NonceAlreadyUsed, TimestampOrderingError, ClockSkew), e:
+ raise Unauthorized('Invalid nonce/timestamp: %s' % e)
+ now = datetime.now(pytz.timezone('UTC'))
+ if token.permission == OAuthPermission.UNAUTHORIZED:
+ raise Unauthorized('Unauthorized token (%s).' % token.key)
+ elif token.date_expires is not None and token.date_expires <= now:
+ raise Unauthorized('Expired token (%s).' % token.key)
+ elif not check_oauth_signature(request, consumer, token):
+ raise Unauthorized('Invalid signature.')
+ else:
+ # Everything is fine, let's return the principal.
+ pass
+ alsoProvides(request, IOAuthSignedRequest)
+ principal = getUtility(IPlacelessLoginSource).getPrincipal(
+ token.person.account.id, access_level=token.permission,
+ scope=token.context)
+
+ return principal
class LaunchpadWebServiceRequestTraversal(WebServiceRequestTraversal):
=== modified file 'lib/canonical/launchpad/zcml/launchpad.zcml'
--- lib/canonical/launchpad/zcml/launchpad.zcml 2010-09-09 21:09:00 +0000
+++ lib/canonical/launchpad/zcml/launchpad.zcml 2010-09-20 17:48:06 +0000
@@ -266,14 +266,14 @@
name="+authorize-token"
class="canonical.launchpad.browser.OAuthAuthorizeTokenView"
template="../templates/oauth-authorize.pt"
- permission="zope.Public" />
+ permission="launchpad.AnyPerson" />
<browser:page
for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication"
name="+token-authorized"
class="canonical.launchpad.browser.OAuthTokenAuthorizedView"
template="../templates/token-authorized.pt"
- permission="zope.Public" />
+ permission="launchpad.AnyPerson" />
<browser:page
for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication"
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2010-09-20 12:56:53 +0000
+++ lib/lp/testing/__init__.py 2010-09-20 17:48:06 +0000
@@ -28,7 +28,6 @@
'map_branch_contents',
'normalize_whitespace',
'oauth_access_token_for',
- 'OAuthSigningBrowser',
'person_logged_in',
'record_statements',
'run_with_login',
@@ -146,7 +145,6 @@
launchpadlib_credentials_for,
launchpadlib_for,
oauth_access_token_for,
- OAuthSigningBrowser,
)
from lp.testing.fixture import ZopeEventHandlerFixture
from lp.testing.matchers import Provides
@@ -224,7 +222,7 @@
class StormStatementRecorder:
"""A storm tracer to count queries.
-
+
This exposes the count and queries as lp.testing._webservice.QueryCollector
does permitting its use with the HasQueryCount matcher.
@@ -683,7 +681,6 @@
def assertTextMatchesExpressionIgnoreWhitespace(self,
regular_expression_txt,
text):
-
def normalise_whitespace(text):
return ' '.join(text.split())
pattern = re.compile(
@@ -860,7 +857,6 @@
callable, and events are the events emitted by the callable.
"""
events = []
-
def on_notify(event):
events.append(event)
old_subscribers = zope.event.subscribers[:]
=== modified file 'lib/lp/testing/_webservice.py'
--- lib/lp/testing/_webservice.py 2010-09-16 15:40:56 +0000
+++ lib/lp/testing/_webservice.py 2010-09-20 17:48:06 +0000
@@ -9,104 +9,34 @@
'launchpadlib_credentials_for',
'launchpadlib_for',
'oauth_access_token_for',
- 'OAuthSigningBrowser',
]
import shutil
import tempfile
+
+from launchpadlib.credentials import (
+ AccessToken,
+ Credentials,
+ )
+from launchpadlib.launchpad import Launchpad
import transaction
-from urllib2 import BaseHandler
-
-from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
-
from zope.app.publication.interfaces import IEndRequestEvent
from zope.app.testing import ztapi
-from zope.testbrowser.testing import Browser
from zope.component import getUtility
import zope.testing.cleanup
-from launchpadlib.credentials import (
- AccessToken,
- Credentials,
- )
-from launchpadlib.launchpad import Launchpad
-
-from lp.testing._login import (
- login,
- logout,
- )
-
from canonical.launchpad.interfaces import (
IOAuthConsumerSet,
IPersonSet,
- OAUTH_REALM,
)
from canonical.launchpad.webapp.adapter import get_request_statements
from canonical.launchpad.webapp.interaction import ANONYMOUS
from canonical.launchpad.webapp.interfaces import OAuthPermission
-
-
-class OAuthSigningHandler(BaseHandler):
- """A urllib2 handler that signs requests with an OAuth token."""
-
- def __init__(self, consumer, token):
- """Constructor
-
- :param consumer: An OAuth consumer.
- :param token: An OAuth token.
- """
- self.consumer = consumer
- self.token = token
-
- def default_open(self, req):
- """Set the Authorization header for the outgoing request."""
- signer = OAuthRequest.from_consumer_and_token(
- self.consumer, self.token)
- signer.sign_request(
- OAuthSignatureMethod_PLAINTEXT(), self.consumer, self.token)
- auth_header = signer.to_header(OAUTH_REALM)['Authorization']
- req.headers['Authorization'] = auth_header
-
-
-class UserAgentFilteringHandler(BaseHandler):
- """A urllib2 handler that replaces the User-Agent header.
-
- [XXX bug=638058] This is a hack to work around a bug in
- zope.testbrowser.
- """
- def __init__(self, user_agent):
- """Constructor."""
- self.user_agent = user_agent
-
- def default_open(self, req):
- """Set the User-Agent header for the outgoing request."""
- req.headers['User-Agent'] = self.user_agent
-
-
-class OAuthSigningBrowser(Browser):
- """A browser that signs each outgoing request with an OAuth token.
-
- This lets us simulate the behavior of the Launchpad Credentials
- Manager.
- """
- def __init__(self, consumer, token, user_agent=None):
- """Constructor.
-
- :param consumer: An OAuth consumer.
- :param token: An OAuth token.
- :param user_agent: The User-Agent string to send.
- """
- super(OAuthSigningBrowser, self).__init__()
- self.mech_browser.add_handler(
- OAuthSigningHandler(consumer, token))
- if user_agent is not None:
- self.mech_browser.add_handler(
- UserAgentFilteringHandler(user_agent))
-
- # This will give us tracebacks instead of unhelpful error
- # messages.
- self.handleErrors = False
+from lp.testing._login import (
+ login,
+ logout,
+ )
def oauth_access_token_for(consumer_name, person, permission, context=None):