Add compute engine credentials (#22)
diff --git a/google/auth/compute_engine/__init__.py b/google/auth/compute_engine/__init__.py
index e3a7f6c..3794be2 100644
--- a/google/auth/compute_engine/__init__.py
+++ b/google/auth/compute_engine/__init__.py
@@ -11,3 +11,12 @@
# 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.
+
+"""Google Compute Engine authentication."""
+
+from google.auth.compute_engine.credentials import Credentials
+
+
+__all__ = [
+ 'Credentials'
+]
diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py
new file mode 100644
index 0000000..f0616d1
--- /dev/null
+++ b/google/auth/compute_engine/credentials.py
@@ -0,0 +1,113 @@
+# Copyright 2016 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.
+
+"""Google Compute Engine credentials.
+
+This module provides authentication for application running on Google Compute
+Engine using the Compute Engine metadata server.
+
+"""
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth.compute_engine import _metadata
+
+
+class Credentials(credentials.Scoped, credentials.Credentials):
+ """Compute Engine Credentials.
+
+ These credentials use the Google Compute Engine metadata server to obtain
+ OAuth 2.0 access tokens associated with the instance's service account.
+
+ For more information about Compute Engine authentication, including how
+ to configure scopes, see the `Compute Engine authentication
+ documentation`_.
+
+ .. note:: Compute Engine instances can be created with scopes and therefore
+ these credentials are considered to be 'scoped'. However, you can
+ not use :meth:`~google.auth.credentials.ScopedCredentials.with_scopes`
+ because it is not possible to change the scopes that the instance
+ has. Also note that
+ :meth:`~google.auth.credentials.ScopedCredentials.has_scopes` will not
+ work until the credentials have been refreshed.
+
+ .. _Compute Engine authentication documentation:
+ https://cloud.google.com/compute/docs/authentication#using
+ """
+
+ def __init__(self, service_account_email='default'):
+ """
+ Args:
+ service_account_email (str): The service account email to use, or
+ 'default'. A Compute Engine instance may have multiple service
+ accounts.
+ """
+ super(Credentials, self).__init__()
+ self._service_account_email = service_account_email
+
+ def _retrieve_info(self, request):
+ """Retrieve information about the service account.
+
+ Updates the scopes and retrieves the full service account email.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+ """
+ info = _metadata.get_service_account_info(
+ request,
+ service_account=self._service_account_email)
+
+ self._service_account_email = info['email']
+ self._scopes = _helpers.string_to_scopes(info['scopes'])
+
+ def refresh(self, request):
+ """Refresh the access token and scopes.
+
+ Args:
+ request (google.auth.transport.Request): The object used to make
+ HTTP requests.
+
+ Raises:
+ google.auth.exceptions.RefreshError: If the Compute Engine metadata
+ service can't be reached if if the instance has not
+ credentials.
+ """
+ try:
+ self._retrieve_info(request)
+ self.token, self.expiry = _metadata.get_service_account_token(
+ request,
+ service_account=self._service_account_email)
+ except exceptions.TransportError as exc:
+ raise exceptions.RefreshError(exc)
+
+ @property
+ def requires_scopes(self):
+ """False: Compute Engine credentials can not be scoped."""
+ return False
+
+ def with_scopes(self, scopes):
+ """Unavailable, Compute Engine credentials can not be scoped.
+
+ Scopes can only be set at Compute Engine instance creation time.
+ See the `Compute Engine authentication documentation`_ for details on
+ how to configure instance scopes.
+
+ .. _Compute Engine authentication documentation:
+ https://cloud.google.com/compute/docs/authentication#using
+ """
+ raise NotImplementedError(
+ 'Compute Engine credentials can not set scopes. Scopes must be '
+ 'set when the Compute Engine instance is created.')
diff --git a/google/auth/credentials.py b/google/auth/credentials.py
index b10e63e..ef28bd7 100644
--- a/google/auth/credentials.py
+++ b/google/auth/credentials.py
@@ -102,7 +102,7 @@
apply the token to the authentication header.
Args:
- request (google.auth.transport.Request): the object used to make
+ request (google.auth.transport.Request): The object used to make
HTTP requests.
method (str): The request's HTTP method.
url (str): The request's URI.
diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py
new file mode 100644
index 0000000..90ce2fe
--- /dev/null
+++ b/tests/compute_engine/test_credentials.py
@@ -0,0 +1,105 @@
+# Copyright 2016 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.
+
+import datetime
+
+import mock
+import pytest
+
+from google.auth import exceptions
+from google.auth.compute_engine import credentials
+
+
+class TestCredentials(object):
+ credentials = None
+
+ @pytest.fixture(autouse=True)
+ def credentials_fixture(self):
+ self.credentials = credentials.Credentials()
+
+ def test_default_state(self):
+ assert not self.credentials.valid
+ # Expiration hasn't been set yet
+ assert not self.credentials.expired
+ # Scopes aren't needed
+ assert not self.credentials.requires_scopes
+
+ @mock.patch(
+ 'google.auth._helpers.utcnow', return_value=datetime.datetime.min)
+ @mock.patch('google.auth.compute_engine._metadata.get')
+ def test_refresh_success(self, get_mock, now_mock):
+ get_mock.side_effect = [{
+ # First request is for sevice account info.
+ 'email': 'service-account@example.com',
+ 'scopes': 'one two'
+ }, {
+ # Second request is for the token.
+ 'access_token': 'token',
+ 'expires_in': 500
+ }]
+
+ # Refresh credentials
+ self.credentials.refresh(None)
+
+ # Check that the credentials have the token and proper expiration
+ assert self.credentials.token == 'token'
+ assert self.credentials.expiry == (
+ datetime.datetime.min + datetime.timedelta(seconds=500))
+
+ # Check the credential info
+ assert (self.credentials._service_account_email ==
+ 'service-account@example.com')
+ assert self.credentials._scopes == ['one', 'two']
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert self.credentials.valid
+
+ @mock.patch('google.auth.compute_engine._metadata.get')
+ def test_refresh_error(self, get_mock):
+ get_mock.side_effect = exceptions.TransportError('http error')
+
+ with pytest.raises(exceptions.RefreshError) as excinfo:
+ self.credentials.refresh(None)
+
+ assert excinfo.match(r'http error')
+
+ @mock.patch('google.auth.compute_engine._metadata.get')
+ def test_before_request_refreshes(self, get_mock):
+ get_mock.side_effect = [{
+ # First request is for sevice account info.
+ 'email': 'service-account@example.com',
+ 'scopes': 'one two'
+ }, {
+ # Second request is for the token.
+ 'access_token': 'token',
+ 'expires_in': 500
+ }]
+
+ # Credentials should start as invalid
+ assert not self.credentials.valid
+
+ # before_request should cause a refresh
+ self.credentials.before_request(
+ mock.Mock(), 'GET', 'http://example.com?a=1#3', {})
+
+ # The refresh endpoint should've been called.
+ assert get_mock.called
+
+ # Credentials should now be valid.
+ assert self.credentials.valid
+
+ def test_with_scopes(self):
+ with pytest.raises(NotImplementedError):
+ self.credentials.with_scopes(['one', 'two'])