Skip to content

Commit 1fbc679

Browse files
salrashid123theacodes
authored andcommitted
Add google.auth.impersonated_credentials (#299)
1 parent e04ee89 commit 1fbc679

6 files changed

+481
-0
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ also provides integration with several HTTP libraries.
1616
- Support for signing and verifying :mod:`JWTs <google.auth.jwt>`.
1717
- Support for verifying and decoding :mod:`ID Tokens <google.oauth2.id_token>`.
1818
- Support for Google :mod:`Service Account credentials <google.oauth2.service_account>`.
19+
- Support for Google :mod:`Impersonated Credentials <google.auth.impersonated_credentials>`.
1920
- Support for :mod:`Google Compute Engine credentials <google.auth.compute_engine>`.
2021
- Support for :mod:`Google App Engine standard credentials <google.auth.app_engine>`.
2122
- Support for various transports, including
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
google.auth.impersonated\_credentials module
2+
============================================
3+
4+
.. automodule:: google.auth.impersonated_credentials
5+
:members:
6+
:inherited-members:
7+
:show-inheritance:

docs/reference/google.auth.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ Submodules
2525
google.auth.environment_vars
2626
google.auth.exceptions
2727
google.auth.iam
28+
google.auth.impersonated_credentials
2829
google.auth.jwt
2930

docs/user-guide.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,35 @@ You can also use :class:`google_auth_oauthlib.flow.Flow` to perform the OAuth
205205
.. _requests-oauthlib:
206206
https://requests-oauthlib.readthedocs.io/en/latest/
207207

208+
Impersonated credentials
209+
++++++++++++++++++++++++
210+
211+
Impersonated Credentials allows one set of credentials issued to a user or service account
212+
to impersonate another. The target service account must grant the source credential
213+
the "Service Account Token Creator" IAM role::
214+
215+
from google.auth import impersonated_credentials
216+
217+
target_scopes = ['https://www.googleapis.com/auth/devstorage.read_only']
218+
source_credentials = service_account.Credentials.from_service_account_file(
219+
'/path/to/svc_account.json',
220+
scopes=target_scopes)
221+
222+
target_credentials = impersonated_credentials.Credentials(
223+
source_credentials=source_credentials,
224+
target_principal='impersonated-account@_project_.iam.gserviceaccount.com',
225+
target_scopes=target_scopes,
226+
lifetime=500)
227+
client = storage.Client(credentials=target_credentials)
228+
buckets = client.list_buckets(project='your_project')
229+
for bucket in buckets:
230+
print bucket.name
231+
232+
233+
In the example above `source_credentials` does not have direct access to list buckets
234+
in the target project. Using `ImpersonatedCredentials` will allow the source_credentials
235+
to assume the identity of a target_principal that does have access
236+
208237
Making authenticated requests
209238
-----------------------------
210239

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Copyright 2018 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Google Cloud Impersonated credentials.
16+
17+
This module provides authentication for applications where local credentials
18+
impersonates a remote service account using `IAM Credentials API`_.
19+
20+
This class can be used to impersonate a service account as long as the original
21+
Credential object has the "Service Account Token Creator" role on the target
22+
service account.
23+
24+
.. _IAM Credentials API:
25+
https://cloud.google.com/iam/credentials/reference/rest/
26+
"""
27+
28+
import copy
29+
from datetime import datetime
30+
import json
31+
32+
import six
33+
from six.moves import http_client
34+
35+
from google.auth import _helpers
36+
from google.auth import credentials
37+
from google.auth import exceptions
38+
39+
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
40+
41+
_IAM_SCOPE = ['https://www.googleapis.com/auth/iam']
42+
43+
_IAM_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/projects/-' +
44+
'/serviceAccounts/{}:generateAccessToken')
45+
46+
_REFRESH_ERROR = 'Unable to acquire impersonated credentials'
47+
_LIFETIME_ERROR = 'Credentials with lifetime set cannot be renewed'
48+
49+
50+
def _make_iam_token_request(request, principal, headers, body):
51+
"""Makes a request to the Google Cloud IAM service for an access token.
52+
Args:
53+
request (Request): The Request object to use.
54+
principal (str): The principal to request an access token for.
55+
headers (Mapping[str, str]): Map of headers to transmit.
56+
body (Mapping[str, str]): JSON Payload body for the iamcredentials
57+
API call.
58+
59+
Raises:
60+
TransportError: Raised if there is an underlying HTTP connection
61+
Error
62+
DefaultCredentialsError: Raised if the impersonated credentials
63+
are not available. Common reasons are
64+
`iamcredentials.googleapis.com` is not enabled or the
65+
`Service Account Token Creator` is not assigned
66+
"""
67+
iam_endpoint = _IAM_ENDPOINT.format(principal)
68+
69+
body = json.dumps(body)
70+
71+
response = request(
72+
url=iam_endpoint,
73+
method='POST',
74+
headers=headers,
75+
body=body)
76+
77+
response_body = response.data.decode('utf-8')
78+
79+
if response.status != http_client.OK:
80+
exceptions.RefreshError(_REFRESH_ERROR, response_body)
81+
82+
try:
83+
token_response = json.loads(response.data.decode('utf-8'))
84+
token = token_response['accessToken']
85+
expiry = datetime.strptime(
86+
token_response['expireTime'], '%Y-%m-%dT%H:%M:%SZ')
87+
88+
return token, expiry
89+
90+
except (KeyError, ValueError) as caught_exc:
91+
new_exc = exceptions.RefreshError(
92+
'{}: No access token or invalid expiration in response.'.format(
93+
_REFRESH_ERROR),
94+
response_body)
95+
six.raise_from(new_exc, caught_exc)
96+
97+
98+
class Credentials(credentials.Credentials):
99+
"""This module defines impersonated credentials which are essentially
100+
impersonated identities.
101+
102+
Impersonated Credentials allows credentials issued to a user or
103+
service account to impersonate another. The target service account must
104+
grant the originating credential principal the
105+
`Service Account Token Creator`_ IAM role:
106+
107+
For more information about Token Creator IAM role and
108+
IAMCredentials API, see
109+
`Creating Short-Lived Service Account Credentials`_.
110+
111+
.. _Service Account Token Creator:
112+
https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role
113+
114+
.. _Creating Short-Lived Service Account Credentials:
115+
https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
116+
117+
Usage:
118+
119+
First grant source_credentials the `Service Account Token Creator`
120+
role on the target account to impersonate. In this example, the
121+
service account represented by svc_account.json has the
122+
token creator role on
123+
`impersonated-account@_project_.iam.gserviceaccount.com`.
124+
125+
Initialize a source credential which does not have access to
126+
list bucket::
127+
128+
from google.oauth2 import service_acccount
129+
130+
target_scopes = [
131+
'https://www.googleapis.com/auth/devstorage.read_only']
132+
133+
source_credentials = (
134+
service_account.Credentials.from_service_account_file(
135+
'/path/to/svc_account.json',
136+
scopes=target_scopes))
137+
138+
Now use the source credentials to acquire credentials to impersonate
139+
another service account::
140+
141+
from google.auth import impersonated_credentials
142+
143+
target_credentials = impersonated_credentials.Credentials(
144+
source_credentials=source_credentials,
145+
target_principal='impersonated-account@_project_.iam.gserviceaccount.com',
146+
target_scopes = target_scopes,
147+
lifetime=500)
148+
149+
Resource access is granted::
150+
151+
client = storage.Client(credentials=target_credentials)
152+
buckets = client.list_buckets(project='your_project')
153+
for bucket in buckets:
154+
print bucket.name
155+
"""
156+
157+
def __init__(self, source_credentials, target_principal,
158+
target_scopes, delegates=None,
159+
lifetime=None):
160+
"""
161+
Args:
162+
source_credentials (google.auth.Credentials): The source credential
163+
used as to acquire the impersonated credentials.
164+
target_principal (str): The service account to impersonate.
165+
target_scopes (Sequence[str]): Scopes to request during the
166+
authorization grant.
167+
delegates (Sequence[str]): The chained list of delegates required
168+
to grant the final access_token. If set, the sequence of
169+
identities must have "Service Account Token Creator" capability
170+
granted to the prceeding identity. For example, if set to
171+
[serviceAccountB, serviceAccountC], the source_credential
172+
must have the Token Creator role on serviceAccountB.
173+
serviceAccountB must have the Token Creator on serviceAccountC.
174+
Finally, C must have Token Creator on target_principal.
175+
If left unset, source_credential must have that role on
176+
target_principal.
177+
lifetime (int): Number of seconds the delegated credential should
178+
be valid for (upto 3600). If set, the credentials will
179+
**not** get refreshed after expiration. If not set, the
180+
credentials will be refreshed every 3600s.
181+
"""
182+
183+
super(Credentials, self).__init__()
184+
185+
self._source_credentials = copy.copy(source_credentials)
186+
self._source_credentials._scopes = _IAM_SCOPE
187+
self._target_principal = target_principal
188+
self._target_scopes = target_scopes
189+
self._delegates = delegates
190+
self._lifetime = lifetime
191+
self.token = None
192+
self.expiry = _helpers.utcnow()
193+
194+
@_helpers.copy_docstring(credentials.Credentials)
195+
def refresh(self, request):
196+
if (self.token is not None and self._lifetime is not None):
197+
self.expiry = _helpers.utcnow()
198+
raise exceptions.RefreshError(_LIFETIME_ERROR)
199+
self._source_credentials.refresh(request)
200+
self._update_token(request)
201+
202+
@property
203+
def expired(self):
204+
return _helpers.utcnow() >= self.expiry
205+
206+
def _update_token(self, request):
207+
"""Updates credentials with a new access_token representing
208+
the impersonated account.
209+
210+
Args:
211+
request (google.auth.transport.requests.Request): Request object
212+
to use for refreshing credentials.
213+
"""
214+
215+
# Refresh our source credentials.
216+
self._source_credentials.refresh(request)
217+
218+
lifetime = self._lifetime
219+
if (self._lifetime is None):
220+
lifetime = _DEFAULT_TOKEN_LIFETIME_SECS
221+
222+
body = {
223+
"delegates": self._delegates,
224+
"scope": self._target_scopes,
225+
"lifetime": str(lifetime) + "s"
226+
}
227+
228+
headers = {
229+
'Content-Type': 'application/json',
230+
}
231+
232+
# Apply the source credentials authentication info.
233+
self._source_credentials.apply(headers)
234+
235+
self.token, self.expiry = _make_iam_token_request(
236+
request=request,
237+
principal=self._target_principal,
238+
headers=headers,
239+
body=body)

0 commit comments

Comments
 (0)