codekingpro/portable-devtools
114k
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 at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.14 15"""Google Cloud Impersonated credentials.16 17This module provides authentication for applications where local credentials18impersonates a remote service account using `IAM Credentials API`_.19 20This class can be used to impersonate a service account as long as the original21Credential object has the "Service Account Token Creator" role on the target22service account.23 24 .. _IAM Credentials API:25 https://cloud.google.com/iam/credentials/reference/rest/26"""27 28import base6429import copy30from datetime import datetime31import http.client as http_client32import json33 34from google.auth import _helpers35from google.auth import credentials36from google.auth import exceptions37from google.auth import jwt38from google.auth import metrics39 40_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]41 42_IAM_ENDPOINT = (43 "https://iamcredentials.googleapis.com/v1/projects/-"44 + "/serviceAccounts/{}:generateAccessToken"45)46 47_IAM_SIGN_ENDPOINT = (48 "https://iamcredentials.googleapis.com/v1/projects/-"49 + "/serviceAccounts/{}:signBlob"50)51 52_IAM_IDTOKEN_ENDPOINT = (53 "https://iamcredentials.googleapis.com/v1/"54 + "projects/-/serviceAccounts/{}:generateIdToken"55)56 57_REFRESH_ERROR = "Unable to acquire impersonated credentials"58 59_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds60 61_DEFAULT_TOKEN_URI = "https://oauth2.googleapis.com/token"62 63 64def _make_iam_token_request(65 request, principal, headers, body, iam_endpoint_override=None66):67 """Makes a request to the Google Cloud IAM service for an access token.68 Args:69 request (Request): The Request object to use.70 principal (str): The principal to request an access token for.71 headers (Mapping[str, str]): Map of headers to transmit.72 body (Mapping[str, str]): JSON Payload body for the iamcredentials73 API call.74 iam_endpoint_override (Optiona[str]): The full IAM endpoint override75 with the target_principal embedded. This is useful when supporting76 impersonation with regional endpoints.77 78 Raises:79 google.auth.exceptions.TransportError: Raised if there is an underlying80 HTTP connection error81 google.auth.exceptions.RefreshError: Raised if the impersonated82 credentials are not available. Common reasons are83 `iamcredentials.googleapis.com` is not enabled or the84 `Service Account Token Creator` is not assigned85 """86 iam_endpoint = iam_endpoint_override or _IAM_ENDPOINT.format(principal)87 88 body = json.dumps(body).encode("utf-8")89 90 response = request(url=iam_endpoint, method="POST", headers=headers, body=body)91 92 # support both string and bytes type response.data93 response_body = (94 response.data.decode("utf-8")95 if hasattr(response.data, "decode")96 else response.data97 )98 99 if response.status != http_client.OK:100 raise exceptions.RefreshError(_REFRESH_ERROR, response_body)101 102 try:103 token_response = json.loads(response_body)104 token = token_response["accessToken"]105 expiry = datetime.strptime(token_response["expireTime"], "%Y-%m-%dT%H:%M:%SZ")106 107 return token, expiry108 109 except (KeyError, ValueError) as caught_exc:110 new_exc = exceptions.RefreshError(111 "{}: No access token or invalid expiration in response.".format(112 _REFRESH_ERROR113 ),114 response_body,115 )116 raise new_exc from caught_exc117 118 119class Credentials(120 credentials.Scoped, credentials.CredentialsWithQuotaProject, credentials.Signing121):122 """This module defines impersonated credentials which are essentially123 impersonated identities.124 125 Impersonated Credentials allows credentials issued to a user or126 service account to impersonate another. The target service account must127 grant the originating credential principal the128 `Service Account Token Creator`_ IAM role:129 130 For more information about Token Creator IAM role and131 IAMCredentials API, see132 `Creating Short-Lived Service Account Credentials`_.133 134 .. _Service Account Token Creator:135 https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role136 137 .. _Creating Short-Lived Service Account Credentials:138 https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials139 140 Usage:141 142 First grant source_credentials the `Service Account Token Creator`143 role on the target account to impersonate. In this example, the144 service account represented by svc_account.json has the145 token creator role on146 `impersonated-account@_project_.iam.gserviceaccount.com`.147 148 Enable the IAMCredentials API on the source project:149 `gcloud services enable iamcredentials.googleapis.com`.150 151 Initialize a source credential which does not have access to152 list bucket::153 154 from google.oauth2 import service_account155 156 target_scopes = [157 'https://www.googleapis.com/auth/devstorage.read_only']158 159 source_credentials = (160 service_account.Credentials.from_service_account_file(161 '/path/to/svc_account.json',162 scopes=target_scopes))163 164 Now use the source credentials to acquire credentials to impersonate165 another service account::166 167 from google.auth import impersonated_credentials168 169 target_credentials = impersonated_credentials.Credentials(170 source_credentials=source_credentials,171 target_principal='impersonated-account@_project_.iam.gserviceaccount.com',172 target_scopes = target_scopes,173 lifetime=500)174 175 Resource access is granted::176 177 client = storage.Client(credentials=target_credentials)178 buckets = client.list_buckets(project='your_project')179 for bucket in buckets:180 print(bucket.name)181 """182 183 def __init__(184 self,185 source_credentials,186 target_principal,187 target_scopes,188 delegates=None,189 lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,190 quota_project_id=None,191 iam_endpoint_override=None,192 ):193 """194 Args:195 source_credentials (google.auth.Credentials): The source credential196 used as to acquire the impersonated credentials.197 target_principal (str): The service account to impersonate.198 target_scopes (Sequence[str]): Scopes to request during the199 authorization grant.200 delegates (Sequence[str]): The chained list of delegates required201 to grant the final access_token. If set, the sequence of202 identities must have "Service Account Token Creator" capability203 granted to the prceeding identity. For example, if set to204 [serviceAccountB, serviceAccountC], the source_credential205 must have the Token Creator role on serviceAccountB.206 serviceAccountB must have the Token Creator on207 serviceAccountC.208 Finally, C must have Token Creator on target_principal.209 If left unset, source_credential must have that role on210 target_principal.211 lifetime (int): Number of seconds the delegated credential should212 be valid for (upto 3600).213 quota_project_id (Optional[str]): The project ID used for quota and billing.214 This project may be different from the project used to215 create the credentials.216 iam_endpoint_override (Optiona[str]): The full IAM endpoint override217 with the target_principal embedded. This is useful when supporting218 impersonation with regional endpoints.219 """220 221 super(Credentials, self).__init__()222 223 self._source_credentials = copy.copy(source_credentials)224 # Service account source credentials must have the _IAM_SCOPE225 # added to refresh correctly. User credentials cannot have226 # their original scopes modified.227 if isinstance(self._source_credentials, credentials.Scoped):228 self._source_credentials = self._source_credentials.with_scopes(_IAM_SCOPE)229 # If the source credential is service account and self signed jwt230 # is needed, we need to create a jwt credential inside it231 if (232 hasattr(self._source_credentials, "_create_self_signed_jwt")233 and self._source_credentials._always_use_jwt_access234 ):235 self._source_credentials._create_self_signed_jwt(None)236 self._target_principal = target_principal237 self._target_scopes = target_scopes238 self._delegates = delegates239 self._lifetime = lifetime or _DEFAULT_TOKEN_LIFETIME_SECS240 self.token = None241 self.expiry = _helpers.utcnow()242 self._quota_project_id = quota_project_id243 self._iam_endpoint_override = iam_endpoint_override244 245 def _metric_header_for_usage(self):246 return metrics.CRED_TYPE_SA_IMPERSONATE247 248 @_helpers.copy_docstring(credentials.Credentials)249 def refresh(self, request):250 self._update_token(request)251 252 def _update_token(self, request):253 """Updates credentials with a new access_token representing254 the impersonated account.255 256 Args:257 request (google.auth.transport.requests.Request): Request object258 to use for refreshing credentials.259 """260 261 # Refresh our source credentials if it is not valid.262 if (263 self._source_credentials.token_state == credentials.TokenState.STALE264 or self._source_credentials.token_state == credentials.TokenState.INVALID265 ):266 self._source_credentials.refresh(request)267 268 body = {269 "delegates": self._delegates,270 "scope": self._target_scopes,271 "lifetime": str(self._lifetime) + "s",272 }273 274 headers = {275 "Content-Type": "application/json",276 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_impersonate(),277 }278 279 # Apply the source credentials authentication info.280 self._source_credentials.apply(headers)281 282 self.token, self.expiry = _make_iam_token_request(283 request=request,284 principal=self._target_principal,285 headers=headers,286 body=body,287 iam_endpoint_override=self._iam_endpoint_override,288 )289 290 def sign_bytes(self, message):291 from google.auth.transport.requests import AuthorizedSession292 293 iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal)294 295 body = {296 "payload": base64.b64encode(message).decode("utf-8"),297 "delegates": self._delegates,298 }299 300 headers = {"Content-Type": "application/json"}301 302 authed_session = AuthorizedSession(self._source_credentials)303 304 try:305 response = authed_session.post(306 url=iam_sign_endpoint, headers=headers, json=body307 )308 finally:309 authed_session.close()310 311 if response.status_code != http_client.OK:312 raise exceptions.TransportError(313 "Error calling sign_bytes: {}".format(response.json())314 )315 316 return base64.b64decode(response.json()["signedBlob"])317 318 @property319 def signer_email(self):320 return self._target_principal321 322 @property323 def service_account_email(self):324 return self._target_principal325 326 @property327 def signer(self):328 return self329 330 @property331 def requires_scopes(self):332 return not self._target_scopes333 334 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)335 def with_quota_project(self, quota_project_id):336 return self.__class__(337 self._source_credentials,338 target_principal=self._target_principal,339 target_scopes=self._target_scopes,340 delegates=self._delegates,341 lifetime=self._lifetime,342 quota_project_id=quota_project_id,343 iam_endpoint_override=self._iam_endpoint_override,344 )345 346 @_helpers.copy_docstring(credentials.Scoped)347 def with_scopes(self, scopes, default_scopes=None):348 return self.__class__(349 self._source_credentials,350 target_principal=self._target_principal,351 target_scopes=scopes or default_scopes,352 delegates=self._delegates,353 lifetime=self._lifetime,354 quota_project_id=self._quota_project_id,355 iam_endpoint_override=self._iam_endpoint_override,356 )357 358 359class IDTokenCredentials(credentials.CredentialsWithQuotaProject):360 """Open ID Connect ID Token-based service account credentials.361 362 """363 364 def __init__(365 self,366 target_credentials,367 target_audience=None,368 include_email=False,369 quota_project_id=None,370 ):371 """372 Args:373 target_credentials (google.auth.Credentials): The target374 credential used as to acquire the id tokens for.375 target_audience (string): Audience to issue the token for.376 include_email (bool): Include email in IdToken377 quota_project_id (Optional[str]): The project ID used for378 quota and billing.379 """380 super(IDTokenCredentials, self).__init__()381 382 if not isinstance(target_credentials, Credentials):383 raise exceptions.GoogleAuthError(384 "Provided Credential must be " "impersonated_credentials"385 )386 self._target_credentials = target_credentials387 self._target_audience = target_audience388 self._include_email = include_email389 self._quota_project_id = quota_project_id390 391 def from_credentials(self, target_credentials, target_audience=None):392 return self.__class__(393 target_credentials=target_credentials,394 target_audience=target_audience,395 include_email=self._include_email,396 quota_project_id=self._quota_project_id,397 )398 399 def with_target_audience(self, target_audience):400 return self.__class__(401 target_credentials=self._target_credentials,402 target_audience=target_audience,403 include_email=self._include_email,404 quota_project_id=self._quota_project_id,405 )406 407 def with_include_email(self, include_email):408 return self.__class__(409 target_credentials=self._target_credentials,410 target_audience=self._target_audience,411 include_email=include_email,412 quota_project_id=self._quota_project_id,413 )414 415 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)416 def with_quota_project(self, quota_project_id):417 return self.__class__(418 target_credentials=self._target_credentials,419 target_audience=self._target_audience,420 include_email=self._include_email,421 quota_project_id=quota_project_id,422 )423 424 @_helpers.copy_docstring(credentials.Credentials)425 def refresh(self, request):426 from google.auth.transport.requests import AuthorizedSession427 428 iam_sign_endpoint = _IAM_IDTOKEN_ENDPOINT.format(429 self._target_credentials.signer_email430 )431 432 body = {433 "audience": self._target_audience,434 "delegates": self._target_credentials._delegates,435 "includeEmail": self._include_email,436 }437 438 headers = {439 "Content-Type": "application/json",440 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_impersonate(),441 }442 443 authed_session = AuthorizedSession(444 self._target_credentials._source_credentials, auth_request=request445 )446 447 try:448 response = authed_session.post(449 url=iam_sign_endpoint,450 headers=headers,451 data=json.dumps(body).encode("utf-8"),452 )453 finally:454 authed_session.close()455 456 if response.status_code != http_client.OK:457 raise exceptions.RefreshError(458 "Error getting ID token: {}".format(response.json())459 )460 461 id_token = response.json()["token"]462 self.token = id_token463 self.expiry = datetime.utcfromtimestamp(464 jwt.decode(id_token, verify=False)["exp"]465 )466 