Skip to content
This repository has been archived by the owner on Jan 18, 2025. It is now read-only.

Update GCE AppAssertionCredentials #476

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b24a4e3
Updated gce.AppAssertionCredentials
elibixby Mar 22, 2016
e4407d2
Typo
elibixby Mar 23, 2016
bcc1198
Move back to JIT field population
elibixby Mar 25, 2016
b721ec8
Move back to 0.1 endpoint
elibixby Mar 25, 2016
73b0a8d
no project/ prefix in 0.1 endpoint
elibixby Mar 25, 2016
96e7250
Moved back to v1 endpoint
elibixby Mar 25, 2016
c0b2300
More specific exception
elibixby Mar 25, 2016
b593ac0
Include scopes and aliases in serialization
elibixby Mar 28, 2016
ae03714
Added tests to green, ready for review
elibixby Mar 29, 2016
eed5f04
python 2.6 fix -_-
elibixby Mar 29, 2016
3f70a0e
Check scopes and error
elibixby Mar 29, 2016
d2f1bbb
Fixed silliness + coverage
elibixby Mar 29, 2016
0737f22
More docstrings
elibixby Mar 29, 2016
81636e6
AttributeError -> ValueError
elibixby Mar 29, 2016
42cc338
Broken doc string
elibixby Mar 29, 2016
099181c
AttributeError -> ValueError in tests
elibixby Mar 29, 2016
1c42eaf
Bad endpoint for metadata server
elibixby Mar 30, 2016
f1a24bf
Still bad metadata endpoint >_<
elibixby Mar 30, 2016
2c7ec57
Cannot use recursive with project-id
elibixby Mar 30, 2016
3406f9c
token endpoint: no recursive but json ='(
elibixby Mar 30, 2016
21e15da
Fix access token call
elibixby Mar 30, 2016
e535203
Revert "Fix access token call"
elibixby Mar 30, 2016
0ee4fa7
Revert "token endpoint: no recursive but json ='("
elibixby Mar 30, 2016
9d582fd
Use content-type to decode
elibixby Mar 30, 2016
5865356
Added test for raw content
elibixby Mar 30, 2016
48e1e9d
Clean up
elibixby Mar 30, 2016
e6e258c
style
elibixby Mar 30, 2016
82092f0
More style fixes
elibixby Mar 30, 2016
b07735b
Doc string fixes
elibixby Mar 30, 2016
a9f2d74
Fix import order
elibixby Mar 31, 2016
55809b0
Alphabetical order for imports
elibixby Mar 31, 2016
15441b9
Accidental import from future commit
elibixby Mar 31, 2016
baada2a
Style + docstring fixes
elibixby Apr 3, 2016
1ffa704
Style fixes
elibixby Apr 4, 2016
ccad050
Docstring and error prose fixes
elibixby Apr 4, 2016
35dc570
Style fix
elibixby Apr 5, 2016
d4a1bfc
More style fixes
elibixby Apr 5, 2016
7afce10
Typo
elibixby Apr 5, 2016
6bb58a3
project id isn't really a property of creds
elibixby Apr 7, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 213 additions & 92 deletions oauth2client/contrib/gce.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,117 @@

Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
"""

import datetime

This comment was marked as spam.

import json
import logging
import warnings

import httplib2
from six.moves import http_client
from six.moves import urllib

from oauth2client._helpers import _from_bytes
from oauth2client import util
from oauth2client.client import HttpAccessTokenRefreshError
from oauth2client.client import AssertionCredentials

from oauth2client._helpers import _from_bytes

__author__ = '[email protected] (Joe Gregorio)'

logger = logging.getLogger(__name__)

# URI Template for the endpoint that returns access_tokens.
_METADATA_ROOT = ('http://metadata.google.internal/computeMetadata/v1/'
'instance/service-accounts/default/')
META = _METADATA_ROOT + 'token'
_DEFAULT_EMAIL_METADATA = _METADATA_ROOT + 'email'
_METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
_SCOPES_WARNING = """\
You have requested explicit scopes to be used with a GCE service account.
Using this argument will have no effect on the actual scopes for tokens
requested. These scopes are set at VM instance creation time and
can't be overridden in the request.
You have requested explicit scopes to be used with a GCE service account, and
these scopes *are* present on the credentials. However, this request will have

This comment was marked as spam.

no effect on the actual scopes for tokens requested. The credentials scopes
are set at VM instance creation time and can't be overridden in the request.

This comment was marked as spam.

To learn more go to https://cloud.google.com/compute/docs/authentication .
"""
_SCOPES_ERROR = """\
You have requested explicit scopes to be used with a GCE service account

This comment was marked as spam.

This comment was marked as spam.

which are not available on the credentials. The scopes are set at VM instance
creation time and can't be overridden in the request.
To learn more go to https://cloud.google.com/compute/docs/authentication .
"""
_NOW = datetime.datetime.now


class MetadataServerHttpError(Exception):
"""Error for Http failures originating from the Metadata Server"""

def _get_service_account_email(http_request=None):
"""Get the GCE service account email from the current environment.

def _get_metadata(http_request=None, path=None, recursive=True):

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

"""Gets a JSON object from the specified path on the Metadata Server

Args:
http_request: callable, (Optional) a callable that matches the method
signature of httplib2.Http.request, used to make
the request to the metadata service.
http_request: an httplib2.Http().request object or equivalent
with which to make the call to the metadata server
path: a list of strings denoting the metadata server request

This comment was marked as spam.

path.
recursive: if true, returns a json blob of the entire tree below
this level. If false, return a list of child keys.

Returns:
tuple, A pair where the first entry is an optional response (from a
failed request) and the second is service account email found (as
a string).
A deserialized JSON object representing the data returned
from the metadata server
"""
if http_request is None:
if path is None:
path = []

if not http_request:

This comment was marked as spam.

http_request = httplib2.Http().request

r_string = '/?recursive=true' if recursive else ''
full_path = _METADATA_ROOT + '/'.join(path) + r_string
response, content = http_request(
_DEFAULT_EMAIL_METADATA, headers={'Metadata-Flavor': 'Google'})
full_path,
headers={'Metadata-Flavor': 'Google'}
)
if response.status == http_client.OK:
content = _from_bytes(content)
return None, content
decoded = _from_bytes(content)
if response['content-type'] == 'application/json':
return json.loads(decoded)
else:
return decoded
else:
return response, content
msg = (
'Failed to retrieve {path} from the Google Compute Engine'
'metadata service. Response:\n{error}'
).format(path=full_path, error=response)
raise MetadataServerHttpError(msg)


def _get_access_token(http_request, email):
"""Get an access token for the specified email from the Metadata Server.

Args:

This comment was marked as spam.

http_request: an httplib2.Http().request object or equivalent
with which to make the call to the metadata server
email: The service account email to request an access token with

Returns:

This comment was marked as spam.

A tuple (access_token, token_expiry). access_token is a string which
can be used as a bearer token to authenticate requests.
token_expiry is a datetime.datetime representing the expiry time
of access_token.
"""
try:
token_json = _get_metadata(
http_request=http_request,
path=[
'instance',
'service-accounts',
email,
'token'
],
recursive=False
)
except MetadataServerHttpError as failed_fetch:
raise HttpAccessTokenRefreshError(str(failed_fetch))

token_expiry = _NOW() + datetime.timedelta(

This comment was marked as spam.

seconds=token_json['expires_in']
)
return token_json['access_token'], token_expiry


class AppAssertionCredentials(AssertionCredentials):
Expand All @@ -85,32 +142,126 @@ class AppAssertionCredentials(AssertionCredentials):
information to generate and refresh its own access tokens.
"""

@util.positional(2)
def __init__(self, scope='', **kwargs):
def __init__(self,
scope=None,
service_account_email='default',

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

service_account_info=None,
**unused_kwargs):
"""Constructor for AppAssertionCredentials

Args:
scope: string or iterable of strings, scope(s) of the credentials
being requested. Using this argument will have no effect on
the actual scopes for tokens requested. These scopes are
set at VM instance creation time and won't change.
scope:
string or iterable of strings, scope(s) of the credentials

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

being requested. Using this argument will have no effect on
the actual scopes for tokens requested. These scopes are
set at VM instance creation time and won't change.
service_account_email:
the email for these credentials. This can be used with custom
service accounts, or left blank to use the default service
account for the instance. The default service account is
usually the shared Compute Engine service account.
service_account_info:
(Optional) Dictionary containing information about a
service account (typically deserialized from JSON).
If passed in, will be used as ``service_account_info``
property. Otherwise, the Compute Engine metadata server
will be used to determine this value. Additionally,
if present the 'email' field in this argument must be
provided and will override the ``service_account_email``
argument.
"""
if scope:
warnings.warn(_SCOPES_WARNING)
# This is just provided for backwards compatibility, but is not
# used by this class.
self.scope = util.scopes_to_string(scope)
self.kwargs = kwargs

# Assertion type is no longer used, but still in the
# parent class signature.
super(AppAssertionCredentials, self).__init__(None)
self._service_account_email = None
super(AppAssertionCredentials, self).__init__(
None,
scopes=scope,

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

**unused_kwargs
)

This comment was marked as spam.

@classmethod
def from_json(cls, json_data):
data = json.loads(_from_bytes(json_data))
return AppAssertionCredentials(data['scope'])
self._service_account_info = service_account_info or {

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

'email': service_account_email
}
self._project_id = None
self._partial = service_account_info is None

self.kwargs = unused_kwargs

# This function call must be made because AssertionCredentials
# will not pass the scopes kwarg to parent class
self._check_scopes_and_notify(scope)

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


@property
def service_account_info(self):
"""Info about this service account.

This property is a deserialized JSON service account object of the form
{'aliases': [...], 'scopes': [...], 'email': '[email protected]'}
where 'scopes' and 'email' will always be present.
"""
return self._get_service_account_info()

@property
def scopes(self):
return self.service_account_info['scopes']

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


@scopes.setter

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

def scopes(self, value):
self._check_scopes_and_notify(value)

@property
def service_account_email(self):

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

return self.service_account_info['email']

@property
def project_id(self):
if not self._project_id:
self._project_id = _get_metadata(
path=['project', 'project-id'],
recursive=False

This comment was marked as spam.

)
return self._project_id

@property
def serialization_data(self):
return {'service_account_info': self.service_account_info}

def _check_scopes_and_notify(self, scopes):
if scopes:
if self.has_scopes(scopes):
warnings.warn(_SCOPES_WARNING)
else:
raise AttributeError(_SCOPES_ERROR)

def _get_service_account_info(self, http_request=None):
"""Retrieves the full info for a service account and caches it

Args:

This comment was marked as spam.

http_request: an httplib2.Http().request object or equivalent
with which to make the call to the metadata server

Returns:

This comment was marked as spam.

A deserialized JSON service account object of the form:
{
'aliases': [...],
'scopes': [...],
'email': '[email protected]'
}
"""
if self._partial:
self._service_account_info = _get_metadata(
path=[
'instance',
'service-accounts',
self._service_account_info['email'],
],
http_request=http_request
)
self._partial = False
return self._service_account_info

def _retrieve_scopes(self, http_request):
return self._get_service_account_info(
http_request=http_request)['scopes']

def _refresh(self, http_request):
"""Refreshes the access_token.
Expand All @@ -125,32 +276,14 @@ def _refresh(self, http_request):
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
"""
response, content = http_request(
META, headers={'Metadata-Flavor': 'Google'})
content = _from_bytes(content)
if response.status == http_client.OK:
try:
token_content = json.loads(content)
except Exception as e:
raise HttpAccessTokenRefreshError(str(e),
status=response.status)
self.access_token = token_content['access_token']
else:
if response.status == http_client.NOT_FOUND:
content += (' This can occur if a VM was created'
' with no service account or scopes.')
raise HttpAccessTokenRefreshError(content, status=response.status)

@property
def serialization_data(self):
raise NotImplementedError(
'Cannot serialize credentials for GCE service accounts.')

def create_scoped_required(self):
return False
self.access_token, self.token_expiry = _get_access_token(
http_request, self._service_account_info['email'])

def create_scoped(self, scopes):
return AppAssertionCredentials(scopes, **self.kwargs)
# Trigger warning or error based on scopes
self.scopes = scopes

This comment was marked as spam.

# No need for new object creation
return self

def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).
Expand All @@ -168,27 +301,15 @@ def sign_blob(self, blob):
raise NotImplementedError(
'Compute Engine service accounts cannot sign blobs')

@property
def service_account_email(self):
"""Get the email for the current service account.

Uses the Google Compute Engine metadata service to retrieve the email
of the default service account.
def to_json(self):
return self._to_json(
self.NON_SERIALIZED_MEMBERS,
to_serialize=self.serialization_data
)

Returns:
string, The email associated with the Google Compute Engine
service account.

Raises:
AttributeError, if the email can not be retrieved from the Google
Compute Engine metadata service.
"""
if self._service_account_email is None:
failure, email = _get_service_account_email()
if failure is None:
self._service_account_email = email
else:
raise AttributeError('Failed to retrieve the email from the '
'Google Compute Engine metadata service',
failure, email)
return self._service_account_email
@classmethod
def from_json(cls, json_data):
data = json.loads(json_data)
return AppAssertionCredentials(
service_account_info=data['service_account_info']
)
Loading