diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py index 6f63831..aebbd32 100644 --- a/oauth2client/appengine.py +++ b/oauth2client/appengine.py @@ -70,7 +70,8 @@ class AppAssertionCredentials(AssertionCredentials): """Constructor for AppAssertionCredentials Args: - scope: string or list of strings, scope(s) of the credentials being requested. + scope: string or list of strings, scope(s) of the credentials being + requested. """ if type(scope) is list: scope = ' '.join(scope) diff --git a/oauth2client/client.py b/oauth2client/client.py index 6b1bdbd..18a6ce1 100644 --- a/oauth2client/client.py +++ b/oauth2client/client.py @@ -624,7 +624,7 @@ class OAuth2Credentials(Credentials): self.invalid = True if self.store: self.store.locked_put(self) - except: + except StandardError: pass raise AccessTokenRefreshError(error_msg) diff --git a/oauth2client/gce.py b/oauth2client/gce.py new file mode 100644 index 0000000..d1b1234 --- /dev/null +++ b/oauth2client/gce.py @@ -0,0 +1,93 @@ +# Copyright (C) 2012 Google Inc. +# +# 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. + +"""Utilities for Google Compute Engine + +Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import httplib2 +import logging +import uritemplate + +from oauth2client import util +from oauth2client.anyjson import simplejson +from oauth2client.client import AccessTokenRefreshError +from oauth2client.client import AssertionCredentials + +logger = logging.getLogger(__name__) + +# URI Template for the endpoint that returns access_tokens. +META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/' + 'default/acquire{?scope}') + + +class AppAssertionCredentials(AssertionCredentials): + """Credentials object for Compute Engine Assertion Grants + + This object will allow a Compute Engine instance to identify itself to + Google and other OAuth 2.0 servers that can verify assertions. It can be used + for the purpose of accessing data stored under an account assigned to the + Compute Engine instance itself. + + This credential does not require a flow to instantiate because it represents + a two legged flow, and therefore has all of the required information to + generate and refresh its own access tokens. + """ + + @util.positional(2) + def __init__(self, scope, **kwargs): + """Constructor for AppAssertionCredentials + + Args: + scope: string or list of strings, scope(s) of the credentials being + requested. + """ + if type(scope) is list: + scope = ' '.join(scope) + self.scope = scope + + super(AppAssertionCredentials, self).__init__( + 'ignored' # assertion_type is ignore in this subclass. + ) + + @classmethod + def from_json(cls, json): + data = simplejson.loads(json) + return AppAssertionCredentials(data['scope']) + + def _refresh(self, http_request): + """Refreshes the access_token. + + Skip all the storage hoops and just refresh using the API. + + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. + + Raises: + AccessTokenRefreshError: When the refresh fails. + """ + uri = uritemplate.expand(META, {'scope': self.scope}) + response, content = http_request(uri) + if response.status == 200: + try: + d = simplejson.loads(content) + except StandardError, e: + raise AccessTokenRefreshError(str(e)) + self.access_token = d['accessToken'] + else: + raise AccessTokenRefreshError(content) diff --git a/runtests.sh b/runtests.sh index b7e8489..a725faf 100755 --- a/runtests.sh +++ b/runtests.sh @@ -20,3 +20,4 @@ $1 runtests.py $FLAGS tests/test_protobuf_model.py $1 runtests.py $FLAGS tests/test_schema.py $1 runtests.py $FLAGS tests/test_oauth2client_appengine.py $1 runtests.py $FLAGS tests/test_oauth2client_keyring.py +$1 runtests.py $FLAGS tests/test_oauth2client_gce.py diff --git a/tests/test_oauth2client_gce.py b/tests/test_oauth2client_gce.py new file mode 100644 index 0000000..15e45c4 --- /dev/null +++ b/tests/test_oauth2client_gce.py @@ -0,0 +1,92 @@ +# Copyright 2012 Google Inc. +# +# 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. + + +"""Tests for oauth2client.gce. + +Unit tests for oauth2client.gce. +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import unittest +import mox + +from oauth2client.client import AccessTokenRefreshError +from oauth2client.client import Credentials +from oauth2client.gce import AppAssertionCredentials + + +class AssertionCredentialsTests(unittest.TestCase): + + def test_good_refresh(self): + m = mox.Mox() + + httplib2_response = m.CreateMock(object) + httplib2_response.status = 200 + + httplib2_request = m.CreateMock(object) + httplib2_request.__call__( + ('http://metadata.google.internal/0.1/meta-data/service-accounts/' + 'default/acquire' + '?scope=http%3A%2F%2Fexample.com%2Fa%20http%3A%2F%2Fexample.com%2Fb' + )).AndReturn((httplib2_response, '{"accessToken": "this-is-a-token"}')) + + m.ReplayAll() + + c = AppAssertionCredentials(scope=['http://example.com/a', + 'http://example.com/b']) + + c._refresh(httplib2_request) + + self.assertEquals('this-is-a-token', c.access_token) + + m.UnsetStubs() + m.VerifyAll() + + + def test_fail_refresh(self): + m = mox.Mox() + + httplib2_response = m.CreateMock(object) + httplib2_response.status = 400 + + httplib2_request = m.CreateMock(object) + httplib2_request.__call__( + ('http://metadata.google.internal/0.1/meta-data/service-accounts/' + 'default/acquire' + '?scope=http%3A%2F%2Fexample.com%2Fa%20http%3A%2F%2Fexample.com%2Fb' + )).AndReturn((httplib2_response, '{"accessToken": "this-is-a-token"}')) + + m.ReplayAll() + + c = AppAssertionCredentials(scope=['http://example.com/a', + 'http://example.com/b']) + + try: + c._refresh(httplib2_request) + self.fail('Should have raised exception on 400') + except AccessTokenRefreshError: + pass + + m.UnsetStubs() + m.VerifyAll() + + def test_to_from_json(self): + c = AppAssertionCredentials(scope=['http://example.com/a', + 'http://example.com/b']) + json = c.to_json() + c2 = Credentials.new_from_json(json) + + self.assertEqual(c.access_token, c2.access_token)