Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds universe domain support for compute credentials #1346

Merged
merged 19 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
155 changes: 125 additions & 30 deletions oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.auth.Credentials;
import com.google.auth.Retryable;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.BufferedReader;
Expand Down Expand Up @@ -79,12 +81,12 @@ public class ComputeEngineCredentials extends GoogleCredentials
implements ServiceAccountSigner, IdTokenProvider {

// Decrease timing margins on GCE.
// This is needed because GCE VMs maintain their own OAuth cache that expires T-5mins, attempting
// This is needed because GCE VMs maintain their own OAuth cache that expires T-4 mins, attempting
// to refresh a token before then, will yield the same stale token. To enable pre-emptive
// refreshes, the margins must be shortened. This shouldn't cause problems since the clock skew
// on the VM and metadata proxy should be non-existent.
static final Duration COMPUTE_EXPIRATION_MARGIN = Duration.ofMinutes(3);
static final Duration COMPUTE_REFRESH_MARGIN = Duration.ofMinutes(4);
static final Duration COMPUTE_REFRESH_MARGIN = Duration.ofMinutes(3).plusSeconds(45);
TimurSadykov marked this conversation as resolved.
Show resolved Hide resolved

private static final Logger LOGGER = Logger.getLogger(ComputeEngineCredentials.class.getName());

Expand Down Expand Up @@ -120,34 +122,31 @@ public class ComputeEngineCredentials extends GoogleCredentials
private transient HttpTransportFactory transportFactory;
private transient String serviceAccountEmail;

private String universeDomainFromMetadata = null;

/**
* Constructor with overridden transport.
* An internal constructor
*
* @param transportFactory HTTP transport factory, creates the transport used to get access
* tokens.
* @param scopes scope strings for the APIs to be called. May be null or an empty collection.
* @param defaultScopes default scope strings for the APIs to be called. May be null or an empty
* collection. Default scopes are ignored if scopes are provided.
* @param builder A builder for {@link ComputeEngineCredentials} See {@link
* ComputeEngineCredentials.Builder}
*/
private ComputeEngineCredentials(
HttpTransportFactory transportFactory,
Collection<String> scopes,
Collection<String> defaultScopes) {
super(/* accessToken= */ null, COMPUTE_REFRESH_MARGIN, COMPUTE_EXPIRATION_MARGIN);
private ComputeEngineCredentials(ComputeEngineCredentials.Builder builder) {
super(builder);

this.transportFactory =
firstNonNull(
transportFactory,
builder.getHttpTransportFactory(),
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
this.transportFactoryClassName = this.transportFactory.getClass().getName();
// Use defaultScopes only when scopes don't exist.
if (scopes == null || scopes.isEmpty()) {
scopes = defaultScopes;
Collection<String> scopesToUse = builder.scopes;
if (scopesToUse == null || scopesToUse.isEmpty()) {
scopesToUse = builder.getDefaultScopes();
}
if (scopes == null) {
if (scopesToUse == null) {
this.scopes = ImmutableSet.<String>of();
} else {
List<String> scopeList = new ArrayList<String>(scopes);
List<String> scopeList = new ArrayList<String>(scopesToUse);
scopeList.removeAll(Arrays.asList("", null));
this.scopes = ImmutableSet.<String>copyOf(scopeList);
}
Expand All @@ -156,14 +155,23 @@ private ComputeEngineCredentials(
/** Clones the compute engine account with the specified scopes. */
@Override
public GoogleCredentials createScoped(Collection<String> newScopes) {
return new ComputeEngineCredentials(this.transportFactory, newScopes, null);
ComputeEngineCredentials.Builder builder =
ComputeEngineCredentials.newBuilder()
.setHttpTransportFactory(transportFactory)
.setScopes(newScopes);
return new ComputeEngineCredentials(builder);
}

/** Clones the compute engine account with the specified scopes. */
/** Clones the compute engine account with the specified scopes and default scopes. */
@Override
public GoogleCredentials createScoped(
Collection<String> newScopes, Collection<String> newDefaultScopes) {
return new ComputeEngineCredentials(this.transportFactory, newScopes, newDefaultScopes);
ComputeEngineCredentials.Builder builder =
ComputeEngineCredentials.newBuilder()
.setHttpTransportFactory(transportFactory)
.setScopes(newScopes)
.setDefaultScopes(newDefaultScopes);
return new ComputeEngineCredentials(builder);
}

/**
Expand All @@ -172,7 +180,7 @@ public GoogleCredentials createScoped(
* @return new ComputeEngineCredentials
*/
public static ComputeEngineCredentials create() {
return new ComputeEngineCredentials(null, null, null);
return new ComputeEngineCredentials(ComputeEngineCredentials.newBuilder());
}

public final Collection<String> getScopes() {
Expand All @@ -192,6 +200,64 @@ String createTokenUrlWithScopes() {
return tokenUrl.toString();
}

/**
* Gets the universe domain from the GCE metadata server.
*
* <p>Returns an explicit universe domain if it was provided during credential initialization.
*
* <p>Returns the {@link Credentials#GOOGLE_DEFAULT_UNIVERSE} if universe domain endpoint is not
* found (404) or returns an empty string.
*
* <p>Otherwise, returns universe domain from GCE metadata service.
*
* <p>Any above value is cached for the credential lifetime.
TimurSadykov marked this conversation as resolved.
Show resolved Hide resolved
*
* @throws IOException if a call to GCE metadata service was unsuccessful. Check if exception
* implements the {@link Retryable} and {@code isRetryable()} will return true if the
* operation may be retried.
* @return string representing a universe domain in the format some-domain.xyz
*/
@Override
public String getUniverseDomain() throws IOException {
if (!isDefaultUniverseDomain()) {
TimurSadykov marked this conversation as resolved.
Show resolved Hide resolved
return super.getUniverseDomain();
}

synchronized (this) {
if (this.universeDomainFromMetadata != null) {
return this.universeDomainFromMetadata;
}
}

String universeDomainFromMetadata = getUniverseDomainFromMetadata();
synchronized (this) {
this.universeDomainFromMetadata = universeDomainFromMetadata;
}
return universeDomainFromMetadata;
}

private String getUniverseDomainFromMetadata() throws IOException {
HttpResponse response = getMetadataResponse(getUniverseDomainUrl());
int statusCode = response.getStatusCode();
if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
return Credentials.GOOGLE_DEFAULT_UNIVERSE;
}
if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
IOException cause =
new IOException(
String.format(
"Unexpected Error code %s trying to get universe domain"
+ " from Compute Engine metadata for the default service account: %s",
statusCode, response.parseAsString()));
throw new GoogleAuthException(true, cause);
}
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved
String responseString = response.parseAsString();
if (responseString.isEmpty()) {
return Credentials.GOOGLE_DEFAULT_UNIVERSE;
TimurSadykov marked this conversation as resolved.
Show resolved Hide resolved
}
return responseString;
}

/** Refresh the access token by getting it from the GCE metadata server */
@Override
public AccessToken refreshAccessToken() throws IOException {
Expand Down Expand Up @@ -420,6 +486,11 @@ public static String getTokenServerEncodedUrl() {
return getTokenServerEncodedUrl(DefaultCredentialsProvider.DEFAULT);
}

public static String getUniverseDomainUrl() {
return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
+ "/computeMetadata/v1/universe/universe_domain";
}

public static String getServiceAccountsUrl() {
return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
+ "/computeMetadata/v1/instance/service-accounts/?recursive=true";
Expand All @@ -436,10 +507,13 @@ public int hashCode() {
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("transportFactoryClassName", transportFactoryClassName)
.toString();
protected ToStringHelper toStringHelper() {
synchronized (this) {
return super.toStringHelper()
.add("transportFactoryClassName", transportFactoryClassName)
.add("scopes", scopes)
.add("universeDomainFromMetadata", universeDomainFromMetadata);
}
}

@Override
Expand All @@ -449,7 +523,8 @@ public boolean equals(Object obj) {
}
ComputeEngineCredentials other = (ComputeEngineCredentials) obj;
return Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
&& Objects.equals(this.scopes, other.scopes);
&& Objects.equals(this.scopes, other.scopes)
&& Objects.equals(this.universeDomainFromMetadata, other.universeDomainFromMetadata);
}

private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
Expand Down Expand Up @@ -542,8 +617,12 @@ private String getDefaultServiceAccount() throws IOException {
public static class Builder extends GoogleCredentials.Builder {
private HttpTransportFactory transportFactory;
private Collection<String> scopes;
private Collection<String> defaultScopes;

protected Builder() {}
protected Builder() {
setRefreshMargin(COMPUTE_REFRESH_MARGIN);
setExpirationMargin(COMPUTE_EXPIRATION_MARGIN);
}

protected Builder(ComputeEngineCredentials credentials) {
this.transportFactory = credentials.transportFactory;
Expand All @@ -562,6 +641,18 @@ public Builder setScopes(Collection<String> scopes) {
return this;
}

@CanIgnoreReturnValue
public Builder setDefaultScopes(Collection<String> defaultScopes) {
this.defaultScopes = defaultScopes;
return this;
}

@CanIgnoreReturnValue
public Builder setUniverseDomain(String universeDomain) {
this.universeDomain = universeDomain;
return this;
}

public HttpTransportFactory getHttpTransportFactory() {
return transportFactory;
}
Expand All @@ -570,8 +661,12 @@ public Collection<String> getScopes() {
return scopes;
}

public Collection<String> getDefaultScopes() {
return defaultScopes;
}

public ComputeEngineCredentials build() {
return new ComputeEngineCredentials(transportFactory, scopes, null);
return new ComputeEngineCredentials(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ public GoogleCredentials(AccessToken accessToken) {
* @param builder an instance of a builder
*/
protected GoogleCredentials(Builder builder) {
super(builder.getAccessToken());
super(builder.getAccessToken(), builder.getRefreshMargin(), builder.getExpirationMargin());
this.quotaProjectId = builder.getQuotaProjectId();

if (builder.universeDomain == null || builder.universeDomain.trim().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ public class OAuth2Credentials extends Credentials {
static final Duration DEFAULT_REFRESH_MARGIN = Duration.ofMinutes(6);
private static final ImmutableMap<String, List<String>> EMPTY_EXTRA_HEADERS = ImmutableMap.of();

private final Duration expirationMargin;
private final Duration refreshMargin;
@VisibleForTesting private final Duration expirationMargin;
@VisibleForTesting private final Duration refreshMargin;

// byte[] is serializable, so the lock variable can be final
@VisibleForTesting final Object lock = new byte[0];
Expand Down
Loading