This looks good; I have only a couple suggestions.
review needsfixing
On Tue, 2009-12-15 at 15:13 +0000, Leonard Richardson wrote:
> === modified file 'lib/canonical/launchpad/pagetests/webservice/xx-service.txt'
> --- lib/canonical/launchpad/pagetests/webservice/xx-service.txt 2009-08-20 04:46:48 +0000
> +++ lib/canonical/launchpad/pagetests/webservice/xx-service.txt 2009-12-15 15:13:21 +0000
> @@ -56,6 +56,39 @@
> HTTP/1.1 404 Not Found
> ...
>
> +== Anonymous requests ==
> +
> +A properly signed web service request whose OAuth token key is empty
> +is treated as an anonymous request.
> +
> + >>> root = 'http://api.launchpad.dev/beta'
> + >>> body = anon_webservice.get(root).jsonBody()
> + >>> print body['projects_collection_link']
> + http://api.launchpad.dev/beta/projects
> + >>> print body['me_link']
> + http://api.launchpad.dev/beta/people/+me
> +
> +Anonymous requests can't access certain data:
> +
> + >>> response = anon_webservice.get(body['me_link'])
> + >>> print response.getheader('status')
> + 401 Unauthorized
> + >>> print response.body
> + You need to be logged in to view this URL.
> + ...
> +
> +Anonymous requests can't change the dataset.
> +
> + >>> import simplejson
> + >>> data = simplejson.dumps({'display_name' : "This won't work"})
> + >>> response = anon_webservice.patch(root + "/~salgado",
> + ... 'application/json', data)
> + >>> print response.getheader('status')
> + 401 Unauthorized
> + >>> print response.body
> + ...
> + Unauthorized: (<Person at...>, 'displayname', 'launchpad.Edit')
> + ...
>
> == API Requests to other hosts ==
>
>
> === modified file 'lib/canonical/launchpad/testing/pages.py'
> --- lib/canonical/launchpad/testing/pages.py 2009-10-16 17:14:42 +0000
> +++ lib/canonical/launchpad/testing/pages.py 2009-12-15 15:13:21 +0000
> @@ -20,7 +20,8 @@
> from BeautifulSoup import (
> BeautifulSoup, CData, Comment, Declaration, NavigableString, PageElement,
> ProcessingInstruction, SoupStrainer, Tag)
> -from contrib.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
> +from contrib.oauth import (
> + OAuthConsumer, OAuthRequest, OAuthSignatureMethod_PLAINTEXT, OAuthToken)
> from urlparse import urljoin
>
> from zope.app.testing.functional import HTTPCaller, SimpleCookie
> @@ -95,10 +96,15 @@
> """
> if oauth_consumer_key is not None and oauth_access_key is not None:
> login(ANONYMOUS)
> - self.consumer = getUtility(IOAuthConsumerSet).getByKey(
> - oauth_consumer_key)
> - self.access_token = self.consumer.getAccessToken(
> - oauth_access_key)
> + consumers = getUtility(IOAuthConsumerSet)
> + self.consumer = consumers.getByKey(oauth_consumer_key)
> + if self.consumer is None:
I find it really confusing that you rely on a non-registered consumer to
identify anonymous access here -- I was expecting you'd use an empty
token key for that.
To do that, though, you'll need to use the 'launchpad-library' as the
consumer key of your anon_webservice below, but when you do that we'll
lose test coverage for the auto-creation of consumers on anonymous
requests. That's no big deal as it can easily be tested by issuing a
request with a manually instantiated LaunchpadWebServiceCaller() (with
an unregistered consumer key and an empty token).
In general we wouldn't be able to create a consumer here because GET
requests have their transactions rolled back. However, webservice
requests always commit their transactions because they need to store
nonces, so we can rely on that to create new consumers here. It might
be worth to add a comment explaining that.
It'd also be nice to extend the extra test I suggested above to show
that the consumer is stored in the database after the request finishes.
> + 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.
> + 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)
>
Hi Leonard,
This looks good; I have only a couple suggestions.
review needsfixing
On Tue, 2009-12-15 at 15:13 +0000, Leonard Richardson wrote: launchpad/ pagetests/ webservice/ xx-service. txt' launchpad/ pagetests/ webservice/ xx-service. txt 2009-08-20 04:46:48 +0000 launchpad/ pagetests/ webservice/ xx-service. txt 2009-12-15 15:13:21 +0000 api.launchpad. dev/beta' .get(root) .jsonBody( ) collection_ link'] api.launchpad. dev/beta/ projects api.launchpad. dev/beta/ people/ +me .get(body[ 'me_link' ]) getheader( 'status' ) dumps({ 'display_ name' : "This won't work"}) .patch( root + "/~salgado", getheader( 'status' ) launchpad/ testing/ pages.py' launchpad/ testing/ pages.py 2009-10-16 17:14:42 +0000 launchpad/ testing/ pages.py 2009-12-15 15:13:21 +0000 uction, SoupStrainer, Tag) ethod_PLAINTEXT ethod_PLAINTEXT , OAuthToken) testing. functional import HTTPCaller, SimpleCookie IOAuthConsumerS et).getByKey( getAccessToken( IOAuthConsumerS et) getByKey( oauth_consumer_ key)
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -56,6 +56,39 @@
> HTTP/1.1 404 Not Found
> ...
>
> +== Anonymous requests ==
> +
> +A properly signed web service request whose OAuth token key is empty
> +is treated as an anonymous request.
> +
> + >>> root = 'http://
> + >>> body = anon_webservice
> + >>> print body['projects_
> + http://
> + >>> print body['me_link']
> + http://
> +
> +Anonymous requests can't access certain data:
> +
> + >>> response = anon_webservice
> + >>> print response.
> + 401 Unauthorized
> + >>> print response.body
> + You need to be logged in to view this URL.
> + ...
> +
> +Anonymous requests can't change the dataset.
> +
> + >>> import simplejson
> + >>> data = simplejson.
> + >>> response = anon_webservice
> + ... 'application/json', data)
> + >>> print response.
> + 401 Unauthorized
> + >>> print response.body
> + ...
> + Unauthorized: (<Person at...>, 'displayname', 'launchpad.Edit')
> + ...
>
> == API Requests to other hosts ==
>
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -20,7 +20,8 @@
> from BeautifulSoup import (
> BeautifulSoup, CData, Comment, Declaration, NavigableString, PageElement,
> ProcessingInstr
> -from contrib.oauth import OAuthRequest, OAuthSignatureM
> +from contrib.oauth import (
> + OAuthConsumer, OAuthRequest, OAuthSignatureM
> from urlparse import urljoin
>
> from zope.app.
> @@ -95,10 +96,15 @@
> """
> if oauth_consumer_key is not None and oauth_access_key is not None:
> login(ANONYMOUS)
> - self.consumer = getUtility(
> - oauth_consumer_key)
> - self.access_token = self.consumer.
> - oauth_access_key)
> + consumers = getUtility(
> + self.consumer = consumers.
> + if self.consumer is None:
I find it really confusing that you rely on a non-registered consumer to
identify anonymous access here -- I was expecting you'd use an empty
token key for that.
To do that, though, you'll need to use the 'launchpad-library' as the viceCaller( ) (with
consumer key of your anon_webservice below, but when you do that we'll
lose test coverage for the auto-creation of consumers on anonymous
requests. That's no big deal as it can easily be tested by issuing a
request with a manually instantiated LaunchpadWebSer
an unregistered consumer key and an empty token).
How does that sound?
> + # Set up for anonymous access oauth_consumer_ key, '') getAccessToken( read-nonprivate ') 'user_webservic e'] = LaunchpadWebSer viceCaller( library' , 'nopriv- read-nonprivate ') 'anon_webservic e'] = LaunchpadWebSer viceCaller( 'setupBrowser' ] = setupBrowser 'browser' ] = setupBrowser() 'anon_browser' ] = setupBrowser() launchpad/ webapp/ servers. py' launchpad/ webapp/ servers. py 2009-10-22 13:16:06 +0000 launchpad/ webapp/ servers. py 2009-12-15 15:13:21 +0000 authorization( request) 'oauth_ consumer_ key') IOAuthConsumerS et).getByKey( consumer_ key) IOAuthConsumerS et) getByKey( consumer_ key) 'oauth_ token') 'Unknown consumer (%s).' % consumer_key) 'oauth_ token') new(consumer_ key, '')
> + self.consumer = OAuthConsumer(
> + self.access_token = OAuthToken('', '')
> + else:
> + self.access_token = self.consumer.
> + oauth_access_key)
> logout()
> else:
> self.consumer = None
> @@ -653,6 +659,8 @@
> 'foobar123451432', 'salgado-
> test.globs[
> 'launchpad-
> + test.globs[
> + 'anonymous-access', '')
> test.globs[
> test.globs[
> test.globs[
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -1177,10 +1177,31 @@
> form = get_oauth_
>
> consumer_key = form.get(
> - consumer = getUtility(
> + consumers = getUtility(
> + consumer = consumers.
> + token_key = form.get(
> + anonymous_request = (token_key == '')
> if consumer is None:
> - raise Unauthorized(
> - token_key = form.get(
> + if anonymous_request:
> + # This is the first time anyone has tried to make an
> + # anonymous request using this consumer
> + # name. Dynamically create the consumer.
> + consumer = consumers.
In general we wouldn't be able to create a consumer here because GET
requests have their transactions rolled back. However, webservice
requests always commit their transactions because they need to store
nonces, so we can rely on that to create new consumers here. It might
be worth to add a comment explaining that.
It'd also be nice to extend the extra test I suggested above to show
that the consumer is stored in the database after the request finishes.
> + else: 'Unknown consumer (%s).' % consumer_key) IPlacelessAuthU tility) unauthenticated Principal( ) getAccessToken( token_key) 'Unknown access token (%s).' % token_key)
> + # An unknown consumer can never make a non-anonymous
> + # request, because access tokens are registered with a
> + # specific, known consumer.
> + raise Unauthorized(
> + 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.
> + auth_utility = getUtility(
> + return auth_utility.
> token = consumer.
> if token is None:
> raise Unauthorized(
>
--
Guilherme Salgado <email address hidden>