-
Notifications
You must be signed in to change notification settings - Fork 431
Update GCE AppAssertionCredentials #476
Changes from 33 commits
b24a4e3
e4407d2
bcc1198
b721ec8
73b0a8d
96e7250
c0b2300
b593ac0
ae03714
eed5f04
3f70a0e
d2f1bbb
0737f22
81636e6
42cc338
099181c
1c42eaf
f1a24bf
2c7ec57
3406f9c
21e15da
e535203
0ee4fa7
9d582fd
5865356
48e1e9d
e6e258c
82092f0
b07735b
a9f2d74
55809b0
15441b9
baada2a
1ffa704
ccad050
35dc570
d4a1bfc
7afce10
6bb58a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,60 +16,117 @@ | |
|
||
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. | ||
""" | ||
|
||
import datetime | ||
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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
"""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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong. |
||
seconds=token_json['expires_in'] | ||
) | ||
return token_json['access_token'], token_expiry | ||
|
||
|
||
class AppAssertionCredentials(AssertionCredentials): | ||
|
@@ -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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
**unused_kwargs | ||
) | ||
|
||
This comment was marked as spam.
Sorry, something went wrong. |
||
@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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
'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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
|
||
@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.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
|
||
@scopes.setter | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
def scopes(self, value): | ||
self._check_scopes_and_notify(value) | ||
|
||
@property | ||
def service_account_email(self): | ||
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong.
This comment was marked as spam.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong. |
||
) | ||
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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong. |
||
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. | ||
|
@@ -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.
Sorry, something went wrong. |
||
# No need for new object creation | ||
return self | ||
|
||
def sign_blob(self, blob): | ||
"""Cryptographically sign a blob (of bytes). | ||
|
@@ -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'] | ||
) |
This comment was marked as spam.
Sorry, something went wrong.