Skip to content

Commit

Permalink
Add google.auth.impersonated_credentials (#299)
Browse files Browse the repository at this point in the history
  • Loading branch information
salrashid123 authored and theacodes committed Nov 9, 2018
1 parent e04ee89 commit 1fbc679
Show file tree
Hide file tree
Showing 6 changed files with 481 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ also provides integration with several HTTP libraries.
- Support for signing and verifying :mod:`JWTs <google.auth.jwt>`.
- Support for verifying and decoding :mod:`ID Tokens <google.oauth2.id_token>`.
- Support for Google :mod:`Service Account credentials <google.oauth2.service_account>`.
- Support for Google :mod:`Impersonated Credentials <google.auth.impersonated_credentials>`.
- Support for :mod:`Google Compute Engine credentials <google.auth.compute_engine>`.
- Support for :mod:`Google App Engine standard credentials <google.auth.app_engine>`.
- Support for various transports, including
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/google.auth.impersonated_credentials.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
google.auth.impersonated\_credentials module
============================================

.. automodule:: google.auth.impersonated_credentials
:members:
:inherited-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/reference/google.auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ Submodules
google.auth.environment_vars
google.auth.exceptions
google.auth.iam
google.auth.impersonated_credentials
google.auth.jwt

29 changes: 29 additions & 0 deletions docs/user-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,35 @@ You can also use :class:`google_auth_oauthlib.flow.Flow` to perform the OAuth
.. _requests-oauthlib:
https://requests-oauthlib.readthedocs.io/en/latest/

Impersonated credentials
++++++++++++++++++++++++

Impersonated Credentials allows one set of credentials issued to a user or service account
to impersonate another. The target service account must grant the source credential
the "Service Account Token Creator" IAM role::

from google.auth import impersonated_credentials

target_scopes = ['https://www.googleapis.com/auth/devstorage.read_only']
source_credentials = service_account.Credentials.from_service_account_file(
'/path/to/svc_account.json',
scopes=target_scopes)

target_credentials = impersonated_credentials.Credentials(
source_credentials=source_credentials,
target_principal='impersonated-account@_project_.iam.gserviceaccount.com',
target_scopes=target_scopes,
lifetime=500)
client = storage.Client(credentials=target_credentials)
buckets = client.list_buckets(project='your_project')
for bucket in buckets:
print bucket.name


In the example above `source_credentials` does not have direct access to list buckets
in the target project. Using `ImpersonatedCredentials` will allow the source_credentials
to assume the identity of a target_principal that does have access

Making authenticated requests
-----------------------------

Expand Down
239 changes: 239 additions & 0 deletions google/auth/impersonated_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# Copyright 2018 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:https://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 Cloud Impersonated credentials.
This module provides authentication for applications where local credentials
impersonates a remote service account using `IAM Credentials API`_.
This class can be used to impersonate a service account as long as the original
Credential object has the "Service Account Token Creator" role on the target
service account.
.. _IAM Credentials API:
https://cloud.google.com/iam/credentials/reference/rest/
"""

import copy
from datetime import datetime
import json

import six
from six.moves import http_client

from google.auth import _helpers
from google.auth import credentials
from google.auth import exceptions

_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds

_IAM_SCOPE = ['https://www.googleapis.com/auth/iam']

_IAM_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/projects/-' +
'/serviceAccounts/{}:generateAccessToken')

_REFRESH_ERROR = 'Unable to acquire impersonated credentials'
_LIFETIME_ERROR = 'Credentials with lifetime set cannot be renewed'


def _make_iam_token_request(request, principal, headers, body):
"""Makes a request to the Google Cloud IAM service for an access token.
Args:
request (Request): The Request object to use.
principal (str): The principal to request an access token for.
headers (Mapping[str, str]): Map of headers to transmit.
body (Mapping[str, str]): JSON Payload body for the iamcredentials
API call.
Raises:
TransportError: Raised if there is an underlying HTTP connection
Error
DefaultCredentialsError: Raised if the impersonated credentials
are not available. Common reasons are
`iamcredentials.googleapis.com` is not enabled or the
`Service Account Token Creator` is not assigned
"""
iam_endpoint = _IAM_ENDPOINT.format(principal)

body = json.dumps(body)

response = request(
url=iam_endpoint,
method='POST',
headers=headers,
body=body)

response_body = response.data.decode('utf-8')

if response.status != http_client.OK:
exceptions.RefreshError(_REFRESH_ERROR, response_body)

try:
token_response = json.loads(response.data.decode('utf-8'))
token = token_response['accessToken']
expiry = datetime.strptime(
token_response['expireTime'], '%Y-%m-%dT%H:%M:%SZ')

return token, expiry

except (KeyError, ValueError) as caught_exc:
new_exc = exceptions.RefreshError(
'{}: No access token or invalid expiration in response.'.format(
_REFRESH_ERROR),
response_body)
six.raise_from(new_exc, caught_exc)


class Credentials(credentials.Credentials):
"""This module defines impersonated credentials which are essentially
impersonated identities.
Impersonated Credentials allows credentials issued to a user or
service account to impersonate another. The target service account must
grant the originating credential principal the
`Service Account Token Creator`_ IAM role:
For more information about Token Creator IAM role and
IAMCredentials API, see
`Creating Short-Lived Service Account Credentials`_.
.. _Service Account Token Creator:
https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role
.. _Creating Short-Lived Service Account Credentials:
https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
Usage:
First grant source_credentials the `Service Account Token Creator`
role on the target account to impersonate. In this example, the
service account represented by svc_account.json has the
token creator role on
`impersonated-account@_project_.iam.gserviceaccount.com`.
Initialize a source credential which does not have access to
list bucket::
from google.oauth2 import service_acccount
target_scopes = [
'https://www.googleapis.com/auth/devstorage.read_only']
source_credentials = (
service_account.Credentials.from_service_account_file(
'/path/to/svc_account.json',
scopes=target_scopes))
Now use the source credentials to acquire credentials to impersonate
another service account::
from google.auth import impersonated_credentials
target_credentials = impersonated_credentials.Credentials(
source_credentials=source_credentials,
target_principal='impersonated-account@_project_.iam.gserviceaccount.com',
target_scopes = target_scopes,
lifetime=500)
Resource access is granted::
client = storage.Client(credentials=target_credentials)
buckets = client.list_buckets(project='your_project')
for bucket in buckets:
print bucket.name
"""

def __init__(self, source_credentials, target_principal,
target_scopes, delegates=None,
lifetime=None):
"""
Args:
source_credentials (google.auth.Credentials): The source credential
used as to acquire the impersonated credentials.
target_principal (str): The service account to impersonate.
target_scopes (Sequence[str]): Scopes to request during the
authorization grant.
delegates (Sequence[str]): The chained list of delegates required
to grant the final access_token. If set, the sequence of
identities must have "Service Account Token Creator" capability
granted to the prceeding identity. For example, if set to
[serviceAccountB, serviceAccountC], the source_credential
must have the Token Creator role on serviceAccountB.
serviceAccountB must have the Token Creator on serviceAccountC.
Finally, C must have Token Creator on target_principal.
If left unset, source_credential must have that role on
target_principal.
lifetime (int): Number of seconds the delegated credential should
be valid for (upto 3600). If set, the credentials will
**not** get refreshed after expiration. If not set, the
credentials will be refreshed every 3600s.
"""

super(Credentials, self).__init__()

self._source_credentials = copy.copy(source_credentials)
self._source_credentials._scopes = _IAM_SCOPE
self._target_principal = target_principal
self._target_scopes = target_scopes
self._delegates = delegates
self._lifetime = lifetime
self.token = None
self.expiry = _helpers.utcnow()

@_helpers.copy_docstring(credentials.Credentials)
def refresh(self, request):
if (self.token is not None and self._lifetime is not None):
self.expiry = _helpers.utcnow()
raise exceptions.RefreshError(_LIFETIME_ERROR)
self._source_credentials.refresh(request)
self._update_token(request)

@property
def expired(self):
return _helpers.utcnow() >= self.expiry

def _update_token(self, request):
"""Updates credentials with a new access_token representing
the impersonated account.
Args:
request (google.auth.transport.requests.Request): Request object
to use for refreshing credentials.
"""

# Refresh our source credentials.
self._source_credentials.refresh(request)

lifetime = self._lifetime
if (self._lifetime is None):
lifetime = _DEFAULT_TOKEN_LIFETIME_SECS

body = {
"delegates": self._delegates,
"scope": self._target_scopes,
"lifetime": str(lifetime) + "s"
}

headers = {
'Content-Type': 'application/json',
}

# Apply the source credentials authentication info.
self._source_credentials.apply(headers)

self.token, self.expiry = _make_iam_token_request(
request=request,
principal=self._target_principal,
headers=headers,
body=body)
Loading

0 comments on commit 1fbc679

Please sign in to comment.