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 configurable token lifetime support #982

Merged
merged 13 commits into from Sep 7, 2022
Expand Up @@ -43,11 +43,13 @@
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand All @@ -73,8 +75,46 @@ abstract static class CredentialSource {
}
}

/**
* Encapsulates the service account impersonation options portion of the configuration for
* ExternalAccountCredentials.
*
* <p>If token_lifetime_seconds is not specified, the library will default to a 1-hour lifetime.
*
* <pre>
* Sample credential source for Pluggable Auth credentials:
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
* {
* ...
* "service_account_impersonation": {
* "token_lifetime_seconds": 2800
* }
* }
* </pre>
*/
static class ServiceAccountImpersonationOptions {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
public final int lifetime;
aeitzman marked this conversation as resolved.
Show resolved Hide resolved

ServiceAccountImpersonationOptions(Map<String, Object> serviceAccountImpersonationOptionsMap) {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
if (serviceAccountImpersonationOptionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) {
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
Object timeout = serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY);
if (timeout instanceof BigDecimal) {
lifetime = ((BigDecimal) timeout).intValue();
} else if (serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY)
instanceof Integer) {
lifetime = (int) timeout;
} else {
lifetime = Integer.parseInt((String) timeout);
}
} else {
lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS;
}
}
}

private static final String CLOUD_PLATFORM_SCOPE =
"https://www.googleapis.com/auth/cloud-platform";
private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds";
aeitzman marked this conversation as resolved.
Show resolved Hide resolved

static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account";
static final String EXECUTABLE_SOURCE_KEY = "executable";
Expand All @@ -85,6 +125,7 @@ abstract static class CredentialSource {
private final String tokenUrl;
private final CredentialSource credentialSource;
private final Collection<String> scopes;
private final ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;

@Nullable private final String tokenInfoUrl;
@Nullable private final String serviceAccountImpersonationUrl;
Expand Down Expand Up @@ -152,7 +193,8 @@ protected ExternalAccountCredentials(
clientId,
clientSecret,
scopes,
/* environmentProvider= */ null);
/* environmentProvider= */ null,
/* serviceAccountImpersonationOptions= */ null);
}

/**
Expand All @@ -175,6 +217,44 @@ protected ExternalAccountCredentials(
@Nullable String clientSecret,
@Nullable Collection<String> scopes,
@Nullable EnvironmentProvider environmentProvider) {
this(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
credentialSource,
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
scopes,
environmentProvider,
/* serviceAccountImpersonationOptions= */ null);
}

/**
* See {@link ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String,
* String, String, CredentialSource, String, String, String, String, String, Collection,
* EnvironmentProvider)}
*
* @param serviceAccountImpersonationOptions additional options for service account impersonation,
* may be null.
*/
protected ExternalAccountCredentials(
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
HttpTransportFactory transportFactory,
String audience,
String subjectTokenType,
String tokenUrl,
CredentialSource credentialSource,
@Nullable String tokenInfoUrl,
@Nullable String serviceAccountImpersonationUrl,
@Nullable String quotaProjectId,
@Nullable String clientId,
@Nullable String clientSecret,
@Nullable Collection<String> scopes,
@Nullable EnvironmentProvider environmentProvider,
@Nullable ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) {
this.transportFactory =
MoreObjects.firstNonNull(
transportFactory,
Expand All @@ -186,6 +266,10 @@ protected ExternalAccountCredentials(
this.credentialSource = checkNotNull(credentialSource);
this.tokenInfoUrl = tokenInfoUrl;
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
this.serviceAccountImpersonationOptions =
serviceAccountImpersonationOptions == null
? new ServiceAccountImpersonationOptions(new HashMap<String, Object>())
: serviceAccountImpersonationOptions;
this.quotaProjectId = quotaProjectId;
this.clientId = clientId;
this.clientSecret = clientSecret;
Expand Down Expand Up @@ -230,6 +314,10 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
builder.environmentProvider == null
? SystemEnvironmentProvider.getInstance()
: builder.environmentProvider;
this.serviceAccountImpersonationOptions =
builder.serviceAccountImpersonationOptions == null
? new ServiceAccountImpersonationOptions(new HashMap<String, Object>())
: builder.serviceAccountImpersonationOptions;

this.workforcePoolUserProject = builder.workforcePoolUserProject;
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
Expand Down Expand Up @@ -275,7 +363,7 @@ ImpersonatedCredentials buildImpersonatedCredentials() {
.setHttpTransportFactory(transportFactory)
.setTargetPrincipal(targetPrincipal)
.setScopes(new ArrayList<>(scopes))
.setLifetime(3600) // 1 hour in seconds
.setLifetime(this.serviceAccountImpersonationOptions.lifetime)
.setIamEndpointOverride(serviceAccountImpersonationUrl)
.build();
}
Expand Down Expand Up @@ -375,6 +463,14 @@ static ExternalAccountCredentials fromJson(
String clientSecret = (String) json.get("client_secret");
String quotaProjectId = (String) json.get("quota_project_id");
String userProject = (String) json.get("workforce_pool_user_project");
Map<String, Object> impersonationOptionsMap =
(Map<String, Object>) json.get("service_account_impersonation");

ServiceAccountImpersonationOptions serviceAccountImpersonationOptions = null;
if (impersonationOptionsMap != null) {
serviceAccountImpersonationOptions =
new ServiceAccountImpersonationOptions(impersonationOptionsMap);
}

if (isAwsCredential(credentialSourceMap)) {
return AwsCredentials.newBuilder()
Expand All @@ -388,6 +484,7 @@ static ExternalAccountCredentials fromJson(
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions)
.build();
} else if (isPluggableAuthCredential(credentialSourceMap)) {
return PluggableAuthCredentials.newBuilder()
Expand All @@ -402,6 +499,7 @@ static ExternalAccountCredentials fromJson(
.setClientId(clientId)
.setClientSecret(clientSecret)
.setWorkforcePoolUserProject(userProject)
.setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions)
.build();
}
return IdentityPoolCredentials.newBuilder()
Expand All @@ -416,6 +514,7 @@ static ExternalAccountCredentials fromJson(
.setClientId(clientId)
.setClientSecret(clientSecret)
.setWorkforcePoolUserProject(userProject)
.setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions)
.build();
}

Expand Down Expand Up @@ -539,6 +638,11 @@ public String getWorkforcePoolUserProject() {
return workforcePoolUserProject;
}

@Nullable
public ServiceAccountImpersonationOptions getServiceAccountImpersonationOptions() {
return serviceAccountImpersonationOptions;
}

EnvironmentProvider getEnvironmentProvider() {
return environmentProvider;
}
Expand Down Expand Up @@ -625,6 +729,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
@Nullable protected String clientSecret;
@Nullable protected Collection<String> scopes;
@Nullable protected String workforcePoolUserProject;
@Nullable protected ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;

protected Builder() {}

Expand All @@ -642,6 +747,7 @@ protected Builder(ExternalAccountCredentials credentials) {
this.scopes = credentials.scopes;
this.environmentProvider = credentials.environmentProvider;
this.workforcePoolUserProject = credentials.workforcePoolUserProject;
this.serviceAccountImpersonationOptions = credentials.serviceAccountImpersonationOptions;
}

/** Sets the HTTP transport factory, creates the transport used to get access tokens. */
Expand Down Expand Up @@ -738,6 +844,12 @@ Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) {
return this;
}

Builder setServiceAccountImpersonationOptions(
aeitzman marked this conversation as resolved.
Show resolved Hide resolved
ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) {
this.serviceAccountImpersonationOptions = serviceAccountImpersonationOptions;
return this;
}

public abstract ExternalAccountCredentials build();
}
}