Skip to content
Permalink
Browse files
fix: service account impersonation with workforce credentials (#770)
* fix: service account impersonation with workforce credentials

* fix: add old constructors

* fix: add one test for service account impersonation with a workforce IdentityPoolCredential

* fix: code review

* fix: remove workforce methods from IdentityPoolCredentials

* fix: can't remove setWorkforcePoolUserProject in Builder
  • Loading branch information
lsirac committed Oct 21, 2021
1 parent ff399e7 commit 6449ef0922053121a6732933ab9e246965fde3b7
@@ -37,7 +37,6 @@
import com.google.api.client.http.HttpResponse;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonParser;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -49,7 +48,6 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
* AWS credentials representing a third-party identity for calling Google APIs.
@@ -114,39 +112,10 @@ static class AwsCredentialSource extends CredentialSource {

private final AwsCredentialSource awsCredentialSource;

/**
* Internal constructor. See {@link
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
* String, CredentialSource, String, String, String, String, String, Collection,
* EnvironmentProvider)}
*/
AwsCredentials(
HttpTransportFactory transportFactory,
String audience,
String subjectTokenType,
String tokenUrl,
AwsCredentialSource credentialSource,
@Nullable String tokenInfoUrl,
@Nullable String serviceAccountImpersonationUrl,
@Nullable String quotaProjectId,
@Nullable String clientId,
@Nullable String clientSecret,
@Nullable Collection<String> scopes,
@Nullable EnvironmentProvider environmentProvider) {
super(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
credentialSource,
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
scopes,
environmentProvider);
this.awsCredentialSource = credentialSource;
/** Internal constructor. See {@link AwsCredentials.Builder}. */
AwsCredentials(Builder builder) {
super(builder);
this.awsCredentialSource = (AwsCredentialSource) builder.credentialSource;
}

@Override
@@ -192,19 +161,7 @@ public String retrieveSubjectToken() throws IOException {
/** Clones the AwsCredentials with the specified scopes. */
@Override
public GoogleCredentials createScoped(Collection<String> newScopes) {
return new AwsCredentials(
transportFactory,
getAudience(),
getSubjectTokenType(),
getTokenUrl(),
awsCredentialSource,
getTokenInfoUrl(),
getServiceAccountImpersonationUrl(),
getQuotaProjectId(),
getClientId(),
getClientSecret(),
newScopes,
getEnvironmentProvider());
return new AwsCredentials((AwsCredentials.Builder) newBuilder(this).setScopes(newScopes));
}

private String retrieveResource(String url, String resourceName) throws IOException {
@@ -342,19 +299,7 @@ public static class Builder extends ExternalAccountCredentials.Builder {

@Override
public AwsCredentials build() {
return new AwsCredentials(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
(AwsCredentialSource) credentialSource,
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
scopes,
environmentProvider);
return new AwsCredentials(this);
}
}
}
@@ -89,14 +89,19 @@ abstract static class CredentialSource {
@Nullable private final String clientId;
@Nullable private final String clientSecret;

// This is used for Workforce Pools. It is passed to STS during token exchange in the
// `options` param and will be embedded in the token by STS.
@Nullable private final String workforcePoolUserProject;

protected transient HttpTransportFactory transportFactory;

@Nullable protected final ImpersonatedCredentials impersonatedCredentials;

private EnvironmentProvider environmentProvider;

/**
* Constructor with minimum identifying information and custom HTTP transport.
* Constructor with minimum identifying information and custom HTTP transport. Does not support
* workforce credentials.
*
* @param transportFactory HTTP transport factory, creates the transport used to get access tokens
* @param audience the STS audience which is usually the fully specified resource name of the
@@ -181,6 +186,49 @@ protected ExternalAccountCredentials(
(scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
this.environmentProvider =
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;
this.workforcePoolUserProject = null;

validateTokenUrl(tokenUrl);
if (serviceAccountImpersonationUrl != null) {
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
}

this.impersonatedCredentials = initializeImpersonatedCredentials();
}

/**
* Internal constructor with minimum identifying information and custom HTTP transport. See {@link
* ExternalAccountCredentials.Builder}.
*/
protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) {
this.transportFactory =
MoreObjects.firstNonNull(
builder.transportFactory,
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName());
this.audience = checkNotNull(builder.audience);
this.subjectTokenType = checkNotNull(builder.subjectTokenType);
this.tokenUrl = checkNotNull(builder.tokenUrl);
this.credentialSource = checkNotNull(builder.credentialSource);
this.tokenInfoUrl = builder.tokenInfoUrl;
this.serviceAccountImpersonationUrl = builder.serviceAccountImpersonationUrl;
this.quotaProjectId = builder.quotaProjectId;
this.clientId = builder.clientId;
this.clientSecret = builder.clientSecret;
this.scopes =
(builder.scopes == null || builder.scopes.isEmpty())
? Arrays.asList(CLOUD_PLATFORM_SCOPE)
: builder.scopes;
this.environmentProvider =
builder.environmentProvider == null
? SystemEnvironmentProvider.getInstance()
: builder.environmentProvider;

this.workforcePoolUserProject = builder.workforcePoolUserProject;
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
throw new IllegalArgumentException(
"The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.");
}

validateTokenUrl(tokenUrl);
if (serviceAccountImpersonationUrl != null) {
@@ -312,23 +360,21 @@ static ExternalAccountCredentials fromJson(
String userProject = (String) json.get("workforce_pool_user_project");

if (isAwsCredential(credentialSourceMap)) {
return new AwsCredentials(
transportFactory,
audience,
subjectTokenType,
tokenUrl,
new AwsCredentialSource(credentialSourceMap),
tokenInfoUrl,
serviceAccountImpersonationUrl,
quotaProjectId,
clientId,
clientSecret,
/* scopes= */ null,
/* environmentProvider= */ null);
return AwsCredentials.newBuilder()
.setHttpTransportFactory(transportFactory)
.setAudience(audience)
.setSubjectTokenType(subjectTokenType)
.setTokenUrl(tokenUrl)
.setTokenInfoUrl(tokenInfoUrl)
.setCredentialSource(new AwsCredentialSource(credentialSourceMap))
.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();
}

return IdentityPoolCredentials.newBuilder()
.setWorkforcePoolUserProject(userProject)
.setHttpTransportFactory(transportFactory)
.setAudience(audience)
.setSubjectTokenType(subjectTokenType)
@@ -339,6 +385,7 @@ static ExternalAccountCredentials fromJson(
.setQuotaProjectId(quotaProjectId)
.setClientId(clientId)
.setClientSecret(clientSecret)
.setWorkforcePoolUserProject(userProject)
.build();
}

@@ -361,13 +408,25 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
return impersonatedCredentials.refreshAccessToken();
}

StsRequestHandler requestHandler =
StsRequestHandler.Builder requestHandler =
StsRequestHandler.newBuilder(
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory())
.setInternalOptions(stsTokenExchangeRequest.getInternalOptions())
.build();
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory());

// If this credential was initialized with a Workforce configuration then the
// workforcePoolUserProject must passed to STS via the the internal options param.
if (isWorkforcePoolConfiguration()) {
GenericJson options = new GenericJson();
options.setFactory(OAuth2Utils.JSON_FACTORY);
options.put("userProject", workforcePoolUserProject);
requestHandler.setInternalOptions(options.toString());
}

if (stsTokenExchangeRequest.getInternalOptions() != null) {
// Overwrite internal options. Let subclass handle setting options.
requestHandler.setInternalOptions(stsTokenExchangeRequest.getInternalOptions());
}

StsTokenExchangeResponse response = requestHandler.exchangeToken();
StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
return response.getAccessToken();
}

@@ -427,10 +486,26 @@ public Collection<String> getScopes() {
return scopes;
}

@Nullable
public String getWorkforcePoolUserProject() {
return workforcePoolUserProject;
}

EnvironmentProvider getEnvironmentProvider() {
return environmentProvider;
}

/**
* Returns whether or not the current configuration is for Workforce Pools (which enable 3p user
* identities, rather than workloads).
*/
public boolean isWorkforcePoolConfiguration() {
Pattern workforceAudiencePattern =
Pattern.compile("^//iam.googleapis.com/locations/.+/workforcePools/.+/providers/.+$");
return workforcePoolUserProject != null
&& workforceAudiencePattern.matcher(getAudience()).matches();
}

static void validateTokenUrl(String tokenUrl) {
List<Pattern> patterns = new ArrayList<>();
patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\.sts\\.googleapis\\.com$"));
@@ -501,6 +576,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
@Nullable protected String clientId;
@Nullable protected String clientSecret;
@Nullable protected Collection<String> scopes;
@Nullable protected String workforcePoolUserProject;

protected Builder() {}

@@ -517,60 +593,95 @@ protected Builder(ExternalAccountCredentials credentials) {
this.clientSecret = credentials.clientSecret;
this.scopes = credentials.scopes;
this.environmentProvider = credentials.environmentProvider;
this.workforcePoolUserProject = credentials.workforcePoolUserProject;
}

/** Sets the HTTP transport factory, creates the transport used to get access tokens. */
public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
this.transportFactory = transportFactory;
return this;
}

/**
* Sets the STS audience which is usually the fully specified resource name of the
* workload/workforce pool provider.
*/
public Builder setAudience(String audience) {
this.audience = audience;
return this;
}

/**
* Sets the STS subject token type based on the OAuth 2.0 token exchange spec. Indicates the
* type of the security token in the credential file.
*/
public Builder setSubjectTokenType(String subjectTokenType) {
this.subjectTokenType = subjectTokenType;
return this;
}

/** Sets the STS token exchange endpoint. */
public Builder setTokenUrl(String tokenUrl) {
this.tokenUrl = tokenUrl;
return this;
}

public Builder setTokenInfoUrl(String tokenInfoUrl) {
this.tokenInfoUrl = tokenInfoUrl;
/** Sets the external credential source. */
public Builder setCredentialSource(CredentialSource credentialSource) {
this.credentialSource = credentialSource;
return this;
}

/**
* Sets the optional URL used for service account impersonation. This is only required when APIs
* to be accessed have not integrated with UberMint. If this is not available, the STS returned
* GCP access token is directly used.
*/
public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) {
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
return this;
}

public Builder setCredentialSource(CredentialSource credentialSource) {
this.credentialSource = credentialSource;
return this;
}

public Builder setScopes(Collection<String> scopes) {
this.scopes = scopes;
/**
* Sets the optional endpoint used to retrieve account related information. Required for gCloud
* session account identification.
*/
public Builder setTokenInfoUrl(String tokenInfoUrl) {
this.tokenInfoUrl = tokenInfoUrl;
return this;
}

/** Sets the optional project used for quota and billing purposes. */
public Builder setQuotaProjectId(String quotaProjectId) {
this.quotaProjectId = quotaProjectId;
return this;
}

/** Sets the optional client ID of the service account from the console. */
public Builder setClientId(String clientId) {
this.clientId = clientId;
return this;
}

/** Sets the optional client secret of the service account from the console. */
public Builder setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
return this;
}

public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
this.transportFactory = transportFactory;
/** Sets the optional scopes to request during the authorization grant. */
public Builder setScopes(Collection<String> scopes) {
this.scopes = scopes;
return this;
}

/**
* Sets the optional workforce pool user project number when the credential corresponds to a
* workforce pool and not a workload identity pool. The underlying principal must still have
* serviceusage.services.use IAM permission to use the project for billing/quota.
*/
public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
this.workforcePoolUserProject = workforcePoolUserProject;
return this;
}

0 comments on commit 6449ef0

Please sign in to comment.