1 /* 2 * Copyright 2015, Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 package com.google.auth.oauth2; 33 34 import static com.google.common.base.MoreObjects.firstNonNull; 35 36 import com.google.api.client.http.GenericUrl; 37 import com.google.api.client.http.HttpHeaders; 38 import com.google.api.client.http.HttpRequest; 39 import com.google.api.client.http.HttpResponse; 40 import com.google.api.client.http.HttpResponseException; 41 import com.google.api.client.http.HttpStatusCodes; 42 import com.google.api.client.json.JsonObjectParser; 43 import com.google.api.client.util.GenericData; 44 import com.google.auth.Credentials; 45 import com.google.auth.Retryable; 46 import com.google.auth.ServiceAccountSigner; 47 import com.google.auth.http.HttpTransportFactory; 48 import com.google.common.annotations.VisibleForTesting; 49 import com.google.common.base.Joiner; 50 import com.google.common.base.MoreObjects.ToStringHelper; 51 import com.google.common.collect.ImmutableSet; 52 import com.google.errorprone.annotations.CanIgnoreReturnValue; 53 import java.io.BufferedReader; 54 import java.io.File; 55 import java.io.IOException; 56 import java.io.InputStream; 57 import java.io.InputStreamReader; 58 import java.io.ObjectInputStream; 59 import java.net.SocketTimeoutException; 60 import java.net.UnknownHostException; 61 import java.time.Duration; 62 import java.util.ArrayList; 63 import java.util.Arrays; 64 import java.util.Collection; 65 import java.util.Collections; 66 import java.util.Date; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Objects; 70 import java.util.logging.Level; 71 import java.util.logging.Logger; 72 73 /** 74 * OAuth2 credentials representing the built-in service account for a Google Compute Engine VM. 75 * 76 * <p>Fetches access tokens from the Google Compute Engine metadata server. 77 * 78 * <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details. 79 */ 80 public class ComputeEngineCredentials extends GoogleCredentials 81 implements ServiceAccountSigner, IdTokenProvider { 82 83 // Decrease timing margins on GCE. 84 // This is needed because GCE VMs maintain their own OAuth cache that expires T-4 mins, attempting 85 // to refresh a token before then, will yield the same stale token. To enable pre-emptive 86 // refreshes, the margins must be shortened. This shouldn't cause problems since the clock skew 87 // on the VM and metadata proxy should be non-existent. 88 static final Duration COMPUTE_EXPIRATION_MARGIN = Duration.ofMinutes(3); 89 static final Duration COMPUTE_REFRESH_MARGIN = Duration.ofMinutes(3).plusSeconds(45); 90 91 private static final Logger LOGGER = Logger.getLogger(ComputeEngineCredentials.class.getName()); 92 93 static final String DEFAULT_METADATA_SERVER_URL = "http://metadata.google.internal"; 94 95 static final String SIGN_BLOB_URL_FORMAT = 96 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; 97 98 // Note: the explicit `timeout` and `tries` below is a workaround. The underlying 99 // issue is that resolving an unknown host on some networks will take 100 // 20-30 seconds; making this timeout short fixes the issue, but 101 // could lead to false negatives in the event that we are on GCE, but 102 // the metadata resolution was particularly slow. The latter case is 103 // "unlikely" since the expected 4-nines time is about 0.5 seconds. 104 // This allows us to limit the total ping maximum timeout to 1.5 seconds 105 // for developer desktop scenarios. 106 static final int MAX_COMPUTE_PING_TRIES = 3; 107 static final int COMPUTE_PING_CONNECTION_TIMEOUT_MS = 500; 108 109 private static final String METADATA_FLAVOR = "Metadata-Flavor"; 110 private static final String GOOGLE = "Google"; 111 private static final String WINDOWS = "windows"; 112 private static final String LINUX = "linux"; 113 114 private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; 115 private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. "; 116 private static final long serialVersionUID = -4113476462526554235L; 117 118 private final String transportFactoryClassName; 119 120 private final Collection<String> scopes; 121 122 private transient HttpTransportFactory transportFactory; 123 private transient String serviceAccountEmail; 124 125 private String universeDomainFromMetadata = null; 126 127 /** 128 * An internal constructor 129 * 130 * @param builder A builder for {@link ComputeEngineCredentials} See {@link 131 * ComputeEngineCredentials.Builder} 132 */ ComputeEngineCredentials(ComputeEngineCredentials.Builder builder)133 private ComputeEngineCredentials(ComputeEngineCredentials.Builder builder) { 134 super(builder); 135 136 this.transportFactory = 137 firstNonNull( 138 builder.getHttpTransportFactory(), 139 getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); 140 this.transportFactoryClassName = this.transportFactory.getClass().getName(); 141 // Use defaultScopes only when scopes don't exist. 142 Collection<String> scopesToUse = builder.scopes; 143 if (scopesToUse == null || scopesToUse.isEmpty()) { 144 scopesToUse = builder.getDefaultScopes(); 145 } 146 if (scopesToUse == null) { 147 this.scopes = ImmutableSet.<String>of(); 148 } else { 149 List<String> scopeList = new ArrayList<String>(scopesToUse); 150 scopeList.removeAll(Arrays.asList("", null)); 151 this.scopes = ImmutableSet.<String>copyOf(scopeList); 152 } 153 } 154 155 /** Clones the compute engine account with the specified scopes. */ 156 @Override createScoped(Collection<String> newScopes)157 public GoogleCredentials createScoped(Collection<String> newScopes) { 158 ComputeEngineCredentials.Builder builder = 159 this.toBuilder().setHttpTransportFactory(transportFactory).setScopes(newScopes); 160 return new ComputeEngineCredentials(builder); 161 } 162 163 /** Clones the compute engine account with the specified scopes and default scopes. */ 164 @Override createScoped( Collection<String> newScopes, Collection<String> newDefaultScopes)165 public GoogleCredentials createScoped( 166 Collection<String> newScopes, Collection<String> newDefaultScopes) { 167 ComputeEngineCredentials.Builder builder = 168 ComputeEngineCredentials.newBuilder() 169 .setHttpTransportFactory(transportFactory) 170 .setScopes(newScopes) 171 .setDefaultScopes(newDefaultScopes); 172 return new ComputeEngineCredentials(builder); 173 } 174 175 /** 176 * Create a new ComputeEngineCredentials instance with default behavior. 177 * 178 * @return new ComputeEngineCredentials 179 */ create()180 public static ComputeEngineCredentials create() { 181 return new ComputeEngineCredentials(ComputeEngineCredentials.newBuilder()); 182 } 183 getScopes()184 public final Collection<String> getScopes() { 185 return scopes; 186 } 187 188 /** 189 * If scopes is specified, add "?scopes=comma-separated-list-of-scopes" to the token url. 190 * 191 * @return token url with the given scopes 192 */ createTokenUrlWithScopes()193 String createTokenUrlWithScopes() { 194 GenericUrl tokenUrl = new GenericUrl(getTokenServerEncodedUrl()); 195 if (!scopes.isEmpty()) { 196 tokenUrl.set("scopes", Joiner.on(',').join(scopes)); 197 } 198 return tokenUrl.toString(); 199 } 200 201 /** 202 * Gets the universe domain from the GCE metadata server. 203 * 204 * <p>Returns an explicit universe domain if it was provided during credential initialization. 205 * 206 * <p>Returns the {@link Credentials#GOOGLE_DEFAULT_UNIVERSE} if universe domain endpoint is not 207 * found (404) or returns an empty string. 208 * 209 * <p>Otherwise, returns universe domain from GCE metadata service. 210 * 211 * <p>Any above value is cached for the credential lifetime. 212 * 213 * @throws IOException if a call to GCE metadata service was unsuccessful. Check if exception 214 * implements the {@link Retryable} and {@code isRetryable()} will return true if the 215 * operation may be retried. 216 * @return string representing a universe domain in the format some-domain.xyz 217 */ 218 @Override getUniverseDomain()219 public String getUniverseDomain() throws IOException { 220 if (isExplicitUniverseDomain()) { 221 return super.getUniverseDomain(); 222 } 223 224 synchronized (this) { 225 if (this.universeDomainFromMetadata != null) { 226 return this.universeDomainFromMetadata; 227 } 228 } 229 230 String universeDomainFromMetadata = getUniverseDomainFromMetadata(); 231 synchronized (this) { 232 this.universeDomainFromMetadata = universeDomainFromMetadata; 233 } 234 return universeDomainFromMetadata; 235 } 236 getUniverseDomainFromMetadata()237 private String getUniverseDomainFromMetadata() throws IOException { 238 HttpResponse response = getMetadataResponse(getUniverseDomainUrl()); 239 int statusCode = response.getStatusCode(); 240 if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { 241 return Credentials.GOOGLE_DEFAULT_UNIVERSE; 242 } 243 if (statusCode != HttpStatusCodes.STATUS_CODE_OK) { 244 IOException cause = 245 new IOException( 246 String.format( 247 "Unexpected Error code %s trying to get universe domain" 248 + " from Compute Engine metadata for the default service account: %s", 249 statusCode, response.parseAsString())); 250 throw new GoogleAuthException(true, cause); 251 } 252 String responseString = response.parseAsString(); 253 254 /* Earlier versions of MDS that supports universe_domain return empty string instead of GDU. */ 255 if (responseString.isEmpty()) { 256 return Credentials.GOOGLE_DEFAULT_UNIVERSE; 257 } 258 return responseString; 259 } 260 261 /** Refresh the access token by getting it from the GCE metadata server */ 262 @Override refreshAccessToken()263 public AccessToken refreshAccessToken() throws IOException { 264 HttpResponse response = getMetadataResponse(createTokenUrlWithScopes()); 265 int statusCode = response.getStatusCode(); 266 if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { 267 throw new IOException( 268 String.format( 269 "Error code %s trying to get security access token from" 270 + " Compute Engine metadata for the default service account. This may be because" 271 + " the virtual machine instance does not have permission scopes specified." 272 + " It is possible to skip checking for Compute Engine metadata by specifying the environment " 273 + " variable " 274 + DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR 275 + "=true.", 276 statusCode)); 277 } 278 if (statusCode != HttpStatusCodes.STATUS_CODE_OK) { 279 throw new IOException( 280 String.format( 281 "Unexpected Error code %s trying to get security access" 282 + " token from Compute Engine metadata for the default service account: %s", 283 statusCode, response.parseAsString())); 284 } 285 InputStream content = response.getContent(); 286 if (content == null) { 287 // Throw explicitly here on empty content to avoid NullPointerException from parseAs call. 288 // Mock transports will have success code with empty content by default. 289 throw new IOException("Empty content from metadata token server request."); 290 } 291 GenericData responseData = response.parseAs(GenericData.class); 292 String accessToken = 293 OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX); 294 int expiresInSeconds = 295 OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); 296 long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000; 297 return new AccessToken(accessToken, new Date(expiresAtMilliseconds)); 298 } 299 300 /** 301 * Returns a Google ID Token from the metadata server on ComputeEngine 302 * 303 * @param targetAudience the aud: field the IdToken should include 304 * @param options list of Credential specific options for the token. For example, an IDToken for a 305 * ComputeEngineCredential could have the full formatted claims returned if 306 * IdTokenProvider.Option.FORMAT_FULL) is provided as a list option. Valid option values are: 307 * <br> 308 * IdTokenProvider.Option.FORMAT_FULL<br> 309 * IdTokenProvider.Option.LICENSES_TRUE<br> 310 * If no options are set, the defaults are "&format=standard&licenses=false" 311 * @throws IOException if the attempt to get an IdToken failed 312 * @return IdToken object which includes the raw id_token, JsonWebSignature 313 */ 314 @Override idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)315 public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options) 316 throws IOException { 317 GenericUrl documentUrl = new GenericUrl(getIdentityDocumentUrl()); 318 if (options != null) { 319 if (options.contains(IdTokenProvider.Option.FORMAT_FULL)) { 320 documentUrl.set("format", "full"); 321 } 322 if (options.contains(IdTokenProvider.Option.LICENSES_TRUE)) { 323 // license will only get returned if format is also full 324 documentUrl.set("format", "full"); 325 documentUrl.set("license", "TRUE"); 326 } 327 } 328 documentUrl.set("audience", targetAudience); 329 HttpResponse response = getMetadataResponse(documentUrl.toString()); 330 InputStream content = response.getContent(); 331 if (content == null) { 332 throw new IOException("Empty content from metadata token server request."); 333 } 334 String rawToken = response.parseAsString(); 335 return IdToken.create(rawToken); 336 } 337 getMetadataResponse(String url)338 private HttpResponse getMetadataResponse(String url) throws IOException { 339 GenericUrl genericUrl = new GenericUrl(url); 340 HttpRequest request = 341 transportFactory.create().createRequestFactory().buildGetRequest(genericUrl); 342 JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); 343 request.setParser(parser); 344 request.getHeaders().set(METADATA_FLAVOR, GOOGLE); 345 request.setThrowExceptionOnExecuteError(false); 346 HttpResponse response; 347 try { 348 response = request.execute(); 349 } catch (UnknownHostException exception) { 350 throw new IOException( 351 "ComputeEngineCredentials cannot find the metadata server. This is" 352 + " likely because code is not running on Google Compute Engine.", 353 exception); 354 } 355 356 if (response.getStatusCode() == 503) { 357 throw GoogleAuthException.createWithTokenEndpointResponseException( 358 new HttpResponseException(response)); 359 } 360 361 return response; 362 } 363 364 /** 365 * Implements an algorithm to detect whether the code is running on Google Compute Environment 366 * (GCE) or equivalent runtime. <a href="https://google.aip.dev/auth/4115">See AIP-4115 for more 367 * details</a> The algorithm consists of active and passive checks: <br> 368 * <b>Active:</b> to check that GCE Metadata service is present by sending a http request to send 369 * a request to {@code ComputeEngineCredentials.DEFAULT_METADATA_SERVER_URL} 370 * 371 * <p><b>Passive:</b> to check if SMBIOS variable is present and contains expected value. This 372 * step is platform specific: 373 * 374 * <p><b>For Linux:</b> check if the file "/sys/class/dmi/id/product_name" exists and contains a 375 * line that starts with Google. 376 * 377 * <p><b>For Windows:</b> to be implemented 378 * 379 * <p><b>Other platforms:</b> not supported 380 * 381 * <p>This algorithm can be disabled with environment variable {@code 382 * DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR} set to {@code true}. In this case, the 383 * algorithm will always return {@code false} Returns {@code true} if currently running on Google 384 * Compute Environment (GCE) or equivalent runtime. Returns {@code false} if detection fails, 385 * platform is not supported or if detection disabled using the environment variable. 386 */ isOnGce( HttpTransportFactory transportFactory, DefaultCredentialsProvider provider)387 static synchronized boolean isOnGce( 388 HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) { 389 // If the environment has requested that we do no GCE checks, return immediately. 390 if (Boolean.parseBoolean(provider.getEnv(DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR))) { 391 return false; 392 } 393 394 boolean result = pingComputeEngineMetadata(transportFactory, provider); 395 396 if (!result) { 397 result = checkStaticGceDetection(provider); 398 } 399 400 if (!result) { 401 LOGGER.log(Level.FINE, "Failed to detect whether running on Google Compute Engine."); 402 } 403 404 return result; 405 } 406 407 @VisibleForTesting checkProductNameOnLinux(BufferedReader reader)408 static boolean checkProductNameOnLinux(BufferedReader reader) throws IOException { 409 String name = reader.readLine().trim(); 410 return name.startsWith(GOOGLE); 411 } 412 413 @VisibleForTesting checkStaticGceDetection(DefaultCredentialsProvider provider)414 static boolean checkStaticGceDetection(DefaultCredentialsProvider provider) { 415 String osName = provider.getOsName(); 416 try { 417 if (osName.startsWith(LINUX)) { 418 // Checks GCE residency on Linux platform. 419 File linuxFile = new File("/sys/class/dmi/id/product_name"); 420 return checkProductNameOnLinux( 421 new BufferedReader(new InputStreamReader(provider.readStream(linuxFile)))); 422 } else if (osName.startsWith(WINDOWS)) { 423 // Checks GCE residency on Windows platform. 424 // TODO: implement registry check via FFI 425 return false; 426 } 427 } catch (IOException e) { 428 LOGGER.log(Level.FINE, "Encountered an unexpected exception when checking SMBIOS value", e); 429 return false; 430 } 431 // Platforms other than Linux and Windows are not supported. 432 return false; 433 } 434 pingComputeEngineMetadata( HttpTransportFactory transportFactory, DefaultCredentialsProvider provider)435 private static boolean pingComputeEngineMetadata( 436 HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) { 437 GenericUrl tokenUrl = new GenericUrl(getMetadataServerUrl(provider)); 438 for (int i = 1; i <= MAX_COMPUTE_PING_TRIES; ++i) { 439 try { 440 HttpRequest request = 441 transportFactory.create().createRequestFactory().buildGetRequest(tokenUrl); 442 request.setConnectTimeout(COMPUTE_PING_CONNECTION_TIMEOUT_MS); 443 request.getHeaders().set(METADATA_FLAVOR, GOOGLE); 444 445 HttpResponse response = request.execute(); 446 try { 447 // Internet providers can return a generic response to all requests, so it is necessary 448 // to check that metadata header is present also. 449 HttpHeaders headers = response.getHeaders(); 450 return OAuth2Utils.headersContainValue(headers, METADATA_FLAVOR, GOOGLE); 451 } finally { 452 response.disconnect(); 453 } 454 } catch (SocketTimeoutException expected) { 455 // Ignore logging timeouts which is the expected failure mode in non GCE environments. 456 } catch (IOException e) { 457 LOGGER.log( 458 Level.FINE, 459 "Encountered an unexpected exception when checking" 460 + " if running on Google Compute Engine using Metadata Service ping.", 461 e); 462 } 463 } 464 return false; 465 } 466 getMetadataServerUrl(DefaultCredentialsProvider provider)467 public static String getMetadataServerUrl(DefaultCredentialsProvider provider) { 468 String metadataServerAddress = 469 provider.getEnv(DefaultCredentialsProvider.GCE_METADATA_HOST_ENV_VAR); 470 if (metadataServerAddress != null) { 471 return "http://" + metadataServerAddress; 472 } 473 return DEFAULT_METADATA_SERVER_URL; 474 } 475 getMetadataServerUrl()476 public static String getMetadataServerUrl() { 477 return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT); 478 } 479 getTokenServerEncodedUrl(DefaultCredentialsProvider provider)480 public static String getTokenServerEncodedUrl(DefaultCredentialsProvider provider) { 481 return getMetadataServerUrl(provider) 482 + "/computeMetadata/v1/instance/service-accounts/default/token"; 483 } 484 getTokenServerEncodedUrl()485 public static String getTokenServerEncodedUrl() { 486 return getTokenServerEncodedUrl(DefaultCredentialsProvider.DEFAULT); 487 } 488 getUniverseDomainUrl()489 public static String getUniverseDomainUrl() { 490 return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT) 491 + "/computeMetadata/v1/universe/universe_domain"; 492 } 493 getServiceAccountsUrl()494 public static String getServiceAccountsUrl() { 495 return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT) 496 + "/computeMetadata/v1/instance/service-accounts/?recursive=true"; 497 } 498 getIdentityDocumentUrl()499 public static String getIdentityDocumentUrl() { 500 return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT) 501 + "/computeMetadata/v1/instance/service-accounts/default/identity"; 502 } 503 504 @Override hashCode()505 public int hashCode() { 506 return Objects.hash(transportFactoryClassName); 507 } 508 509 @Override toStringHelper()510 protected ToStringHelper toStringHelper() { 511 synchronized (this) { 512 return super.toStringHelper() 513 .add("transportFactoryClassName", transportFactoryClassName) 514 .add("scopes", scopes) 515 .add("universeDomainFromMetadata", universeDomainFromMetadata); 516 } 517 } 518 519 @Override equals(Object obj)520 public boolean equals(Object obj) { 521 if (!(obj instanceof ComputeEngineCredentials)) { 522 return false; 523 } 524 if (!super.equals(obj)) { 525 return false; 526 } 527 ComputeEngineCredentials other = (ComputeEngineCredentials) obj; 528 return Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) 529 && Objects.equals(this.scopes, other.scopes) 530 && Objects.equals(this.universeDomainFromMetadata, other.universeDomainFromMetadata); 531 } 532 readObject(ObjectInputStream input)533 private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { 534 input.defaultReadObject(); 535 transportFactory = newInstance(transportFactoryClassName); 536 } 537 538 @Override toBuilder()539 public Builder toBuilder() { 540 return new Builder(this); 541 } 542 newBuilder()543 public static Builder newBuilder() { 544 return new Builder(); 545 } 546 547 /** 548 * Returns the email address associated with the GCE default service account. 549 * 550 * @throws RuntimeException if the default service account cannot be read 551 */ 552 @Override 553 // todo(#314) getAccount should not throw a RuntimeException getAccount()554 public String getAccount() { 555 if (serviceAccountEmail == null) { 556 try { 557 serviceAccountEmail = getDefaultServiceAccount(); 558 } catch (IOException ex) { 559 throw new RuntimeException("Failed to get service account", ex); 560 } 561 } 562 return serviceAccountEmail; 563 } 564 565 /** 566 * Signs the provided bytes using the private key associated with the service account. 567 * 568 * <p>The Compute Engine's project must enable the Identity and Access Management (IAM) API and 569 * the instance's service account must have the iam.serviceAccounts.signBlob permission. 570 * 571 * @param toSign bytes to sign 572 * @return signed bytes 573 * @throws SigningException if the attempt to sign the provided bytes failed 574 * @see <a 575 * href="https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob">Blob 576 * Signing</a> 577 */ 578 @Override sign(byte[] toSign)579 public byte[] sign(byte[] toSign) { 580 try { 581 String account = getAccount(); 582 return IamUtils.sign( 583 account, this, transportFactory.create(), toSign, Collections.<String, Object>emptyMap()); 584 } catch (SigningException ex) { 585 throw ex; 586 } catch (RuntimeException ex) { 587 throw new SigningException("Signing failed", ex); 588 } 589 } 590 getDefaultServiceAccount()591 private String getDefaultServiceAccount() throws IOException { 592 HttpResponse response = getMetadataResponse(getServiceAccountsUrl()); 593 int statusCode = response.getStatusCode(); 594 if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { 595 throw new IOException( 596 String.format( 597 "Error code %s trying to get service accounts from" 598 + " Compute Engine metadata. This may be because the virtual machine instance" 599 + " does not have permission scopes specified.", 600 statusCode)); 601 } 602 if (statusCode != HttpStatusCodes.STATUS_CODE_OK) { 603 throw new IOException( 604 String.format( 605 "Unexpected Error code %s trying to get service accounts" 606 + " from Compute Engine metadata: %s", 607 statusCode, response.parseAsString())); 608 } 609 InputStream content = response.getContent(); 610 if (content == null) { 611 // Throw explicitly here on empty content to avoid NullPointerException from parseAs call. 612 // Mock transports will have success code with empty content by default. 613 throw new IOException("Empty content from metadata token server request."); 614 } 615 GenericData responseData = response.parseAs(GenericData.class); 616 Map<String, Object> defaultAccount = 617 OAuth2Utils.validateMap(responseData, "default", PARSE_ERROR_ACCOUNT); 618 return OAuth2Utils.validateString(defaultAccount, "email", PARSE_ERROR_ACCOUNT); 619 } 620 621 public static class Builder extends GoogleCredentials.Builder { 622 private HttpTransportFactory transportFactory; 623 private Collection<String> scopes; 624 private Collection<String> defaultScopes; 625 Builder()626 protected Builder() { 627 setRefreshMargin(COMPUTE_REFRESH_MARGIN); 628 setExpirationMargin(COMPUTE_EXPIRATION_MARGIN); 629 } 630 Builder(ComputeEngineCredentials credentials)631 protected Builder(ComputeEngineCredentials credentials) { 632 super(credentials); 633 this.transportFactory = credentials.transportFactory; 634 this.scopes = credentials.scopes; 635 } 636 637 @CanIgnoreReturnValue setHttpTransportFactory(HttpTransportFactory transportFactory)638 public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { 639 this.transportFactory = transportFactory; 640 return this; 641 } 642 643 @CanIgnoreReturnValue setScopes(Collection<String> scopes)644 public Builder setScopes(Collection<String> scopes) { 645 this.scopes = scopes; 646 return this; 647 } 648 649 @CanIgnoreReturnValue setDefaultScopes(Collection<String> defaultScopes)650 public Builder setDefaultScopes(Collection<String> defaultScopes) { 651 this.defaultScopes = defaultScopes; 652 return this; 653 } 654 655 @CanIgnoreReturnValue setUniverseDomain(String universeDomain)656 public Builder setUniverseDomain(String universeDomain) { 657 this.universeDomain = universeDomain; 658 return this; 659 } 660 661 @CanIgnoreReturnValue setQuotaProjectId(String quotaProjectId)662 public Builder setQuotaProjectId(String quotaProjectId) { 663 super.quotaProjectId = quotaProjectId; 664 return this; 665 } 666 getHttpTransportFactory()667 public HttpTransportFactory getHttpTransportFactory() { 668 return transportFactory; 669 } 670 getScopes()671 public Collection<String> getScopes() { 672 return scopes; 673 } 674 getDefaultScopes()675 public Collection<String> getDefaultScopes() { 676 return defaultScopes; 677 } 678 679 @Override build()680 public ComputeEngineCredentials build() { 681 return new ComputeEngineCredentials(this); 682 } 683 } 684 } 685