diff --git a/google/auth/compute_engine/__init__.py b/google/auth/compute_engine/__init__.py index e3a7f6c35..3794be2f7 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 000000000..f0616d148 --- /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 b10e63e14..ef28bd792 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -102,7 +102,7 @@ def before_request(self, request, method, url, headers): 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 000000000..90ce2fece --- /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'])