Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.api.core.InternalApi;
import com.google.auth.CredentialTypeForMetrics;
import com.google.auth.Credentials;
import com.google.auth.Retryable;
Expand Down Expand Up @@ -71,6 +72,7 @@
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
* OAuth2 credentials representing the built-in service account for a Google Compute Engine VM.
Expand All @@ -80,7 +82,7 @@
* <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
*/
public class ComputeEngineCredentials extends GoogleCredentials
implements ServiceAccountSigner, IdTokenProvider {
implements ServiceAccountSigner, IdTokenProvider, RegionalAccessBoundaryProvider {

static final String METADATA_RESPONSE_EMPTY_CONTENT_ERROR_MESSAGE =
"Empty content from metadata token server request.";
Expand Down Expand Up @@ -116,6 +118,7 @@ public class ComputeEngineCredentials extends GoogleCredentials

private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$");
private static final long serialVersionUID = -4113476462526554235L;

private final String transportFactoryClassName;
Expand Down Expand Up @@ -454,7 +457,6 @@ public AccessToken refreshAccessToken() throws IOException {
int expiresInSeconds =
OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;

return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
}

Expand Down Expand Up @@ -779,6 +781,11 @@ public static Builder newBuilder() {
*
* @throws RuntimeException if the default service account cannot be read
*/
@Override
HttpTransportFactory getTransportFactory() {
return transportFactory;
}

@Override
// todo(#314) getAccount should not throw a RuntimeException
public String getAccount() {
Expand All @@ -792,6 +799,24 @@ public String getAccount() {
return principal;
}

@InternalApi
@Override
public String getRegionalAccessBoundaryUrl() throws IOException {
String account = getAccount();
// The MDS may return a non-email value for the account and we should skip RAB refresh in that
// scenario.
if (account == null || !EMAIL_PATTERN.matcher(account).matches()) {
LoggingUtils.log(
LOGGER_PROVIDER,
Level.INFO,
Collections.emptyMap(),
"Unable to retrieve this instance's email and will skip the regional request routing. Proceeding with request");
return null;
}
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, account);
}

/**
* Signs the provided bytes using the private key associated with the service account.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@

package com.google.auth.oauth2;

import static com.google.auth.oauth2.OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL;
import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
import static com.google.auth.oauth2.OAuth2Utils.WORKFORCE_AUDIENCE_PATTERN;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
Expand All @@ -43,6 +45,7 @@
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Preconditions;
import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.base.MoreObjects;
import com.google.common.io.BaseEncoding;
Expand All @@ -54,6 +57,7 @@
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import javax.annotation.Nullable;

/**
Expand All @@ -74,7 +78,8 @@
* }
* </pre>
*/
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials {
public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials
implements RegionalAccessBoundaryProvider {
private static final LoggerProvider LOGGER_PROVIDER =
LoggerProvider.forClazz(ExternalAccountAuthorizedUserCredentials.class);

Expand Down Expand Up @@ -229,6 +234,24 @@ public AccessToken refreshAccessToken() throws IOException {
.build();
}

@InternalApi
@Override
public String getRegionalAccessBoundaryUrl() throws IOException {
Matcher matcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
if (!matcher.matches()) {
throw new IllegalStateException(
"The provided audience is not in the correct format for a workforce pool. "
+ "Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
}
Comment on lines +240 to +245

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getAudience() method is annotated with @Nullable and can return null. Calling WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience()) directly without a null check will throw a NullPointerException if the audience is null. We should add a null check and throw an IllegalStateException with a clear message.

    String audience = getAudience();
    if (audience == null) {
      throw new IllegalStateException(
          "The audience is null, which is not in the correct format for a workforce pool.");
    }
    Matcher matcher = WORKFORCE_AUDIENCE_PATTERN.matcher(audience);
    if (!matcher.matches()) {
      throw new IllegalStateException(
          "The provided audience is not in the correct format for a workforce pool. "
              + "Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
    }

String poolId = matcher.group("pool");
return String.format(IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, poolId);
}

@Override
HttpTransportFactory getTransportFactory() {
return transportFactory;
}

@Nullable
public String getAudience() {
return audience;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@

package com.google.auth.oauth2;

import static com.google.auth.oauth2.OAuth2Utils.WORKFORCE_AUDIENCE_PATTERN;
import static com.google.auth.oauth2.OAuth2Utils.WORKLOAD_AUDIENCE_PATTERN;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.client.http.HttpHeaders;
import com.google.api.client.json.GenericJson;
import com.google.api.client.util.Data;
import com.google.api.core.InternalApi;
import com.google.auth.RequestMetadataCallback;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.base.MoreObjects;
Expand All @@ -54,6 +57,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

Expand All @@ -63,7 +67,8 @@
* <p>Handles initializing external credentials, calls to the Security Token Service, and service
* account impersonation.
*/
public abstract class ExternalAccountCredentials extends GoogleCredentials {
public abstract class ExternalAccountCredentials extends GoogleCredentials
implements RegionalAccessBoundaryProvider {

private static final long serialVersionUID = 8049126194174465023L;

Expand Down Expand Up @@ -577,6 +582,11 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
*/
public abstract String retrieveSubjectToken() throws IOException;

@Override
HttpTransportFactory getTransportFactory() {
return transportFactory;
}

public String getAudience() {
return audience;
}
Expand Down Expand Up @@ -620,6 +630,37 @@ public String getServiceAccountEmail() {
return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
}

@InternalApi
@Override
public String getRegionalAccessBoundaryUrl() throws IOException {
if (getServiceAccountEmail() != null) {
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
getServiceAccountEmail());
}

Matcher workforceMatcher = WORKFORCE_AUDIENCE_PATTERN.matcher(getAudience());
if (workforceMatcher.matches()) {
String poolId = workforceMatcher.group("pool");
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, poolId);
}

Matcher workloadMatcher = WORKLOAD_AUDIENCE_PATTERN.matcher(getAudience());
Comment on lines +642 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getAudience() method can return null. Storing the audience in a local variable and checking it for null first prevents a potential NullPointerException and avoids redundant calls to getAudience() on the validation path.

    String audience = getAudience();
    if (audience == null) {
      throw new IllegalStateException(
          "The audience is null, which is not in a valid format for either a workload identity pool or a workforce pool.");
    }

    Matcher workforceMatcher = WORKFORCE_AUDIENCE_PATTERN.matcher(audience);
    if (workforceMatcher.matches()) {
      String poolId = workforceMatcher.group("pool");
      return String.format(
          OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKFORCE_POOL, poolId);
    }

    Matcher workloadMatcher = WORKLOAD_AUDIENCE_PATTERN.matcher(audience);

if (workloadMatcher.matches()) {
String projectNumber = workloadMatcher.group("project");
String poolId = workloadMatcher.group("pool");
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_WORKLOAD_POOL,
projectNumber,
poolId);
}

throw new IllegalStateException(
"The provided audience is not in a valid format for either a workload identity pool or a workforce pool."
+ " Refer: https://docs.cloud.google.com/iam/docs/principal-identifiers");
}

@Nullable
public String getClientId() {
return clientId;
Expand Down
Loading
Loading