From 6449ef0922053121a6732933ab9e246965fde3b7 Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Thu, 21 Oct 2021 11:29:39 -0700 Subject: [PATCH] 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 --- .../google/auth/oauth2/AwsCredentials.java | 67 +---- .../oauth2/ExternalAccountCredentials.java | 173 +++++++++-- .../auth/oauth2/IdentityPoolCredentials.java | 55 +--- .../ExternalAccountCredentialsTest.java | 282 ++++++++++++------ .../oauth2/IdentityPoolCredentialsTest.java | 75 +++-- ...ckExternalAccountCredentialsTransport.java | 9 + 6 files changed, 404 insertions(+), 257 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 25d67660d..54c7a13e9 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -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 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 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); } } } diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 38b4d37ff..547a04261 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -89,6 +89,10 @@ 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; @@ -96,7 +100,8 @@ abstract static class CredentialSource { 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 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 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 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 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 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; } diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index d13cd8545..44e9c0e93 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -53,7 +53,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.regex.Pattern; import javax.annotation.Nullable; /** @@ -155,37 +154,10 @@ private boolean hasHeaders() { private final IdentityPoolCredentialSource identityPoolCredentialSource; - // 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 String workforcePoolUserProject; - /** Internal constructor. See {@link Builder}. */ IdentityPoolCredentials(Builder builder) { - super( - builder.transportFactory, - builder.audience, - builder.subjectTokenType, - builder.tokenUrl, - builder.credentialSource, - builder.tokenInfoUrl, - builder.serviceAccountImpersonationUrl, - builder.quotaProjectId, - builder.clientId, - builder.clientSecret, - builder.scopes, - builder.environmentProvider); + super(builder); this.identityPoolCredentialSource = (IdentityPoolCredentialSource) builder.credentialSource; - 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."); - } - } - - @Nullable - public String getWorkforcePoolUserProject() { - return workforcePoolUserProject; } @Override @@ -200,15 +172,6 @@ public AccessToken refreshAccessToken() throws IOException { stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); } - // 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); - stsTokenExchangeRequest.setInternalOptions(options.toString()); - } - return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build()); } @@ -276,17 +239,6 @@ private String getSubjectTokenFromMetadataServer() throws IOException { } } - /** - * 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(); - } - /** Clones the IdentityPoolCredentials with the specified scopes. */ @Override public IdentityPoolCredentials createScoped(Collection newScopes) { @@ -304,17 +256,14 @@ public static Builder newBuilder(IdentityPoolCredentials identityPoolCredentials public static class Builder extends ExternalAccountCredentials.Builder { - @Nullable private String workforcePoolUserProject; - Builder() {} Builder(IdentityPoolCredentials credentials) { super(credentials); - setWorkforcePoolUserProject(credentials.getWorkforcePoolUserProject()); } public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) { - this.workforcePoolUserProject = workforcePoolUserProject; + super.setWorkforcePoolUserProject(workforcePoolUserProject); return this; } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 0ba3efa52..c59560f56 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -31,6 +31,7 @@ package com.google.auth.oauth2; +import static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -46,13 +47,11 @@ import java.io.IOException; import java.net.URI; import java.util.Arrays; -import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,6 +60,13 @@ public class ExternalAccountCredentialsTest { private static final String STS_URL = "https://sts.googleapis.com"; + private static final Map FILE_CREDENTIAL_SOURCE_MAP = + new HashMap() { + { + put("file", "file"); + } + }; + static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory { MockExternalAccountCredentialsTransport transport = @@ -262,51 +268,134 @@ void fromJson_invalidWorkforceAudiences_throws() { } @Test - void constructor_invalidTokenUrl() { + void constructor_builder() { + HashMap credentialSource = new HashMap<>(); + credentialSource.put("file", "file"); + + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("https://tokeninfo.com") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setCredentialSource(new TestCredentialSource(credentialSource)) + .setScopes(Arrays.asList("scope1", "scope2")) + .setQuotaProjectId("projectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setWorkforcePoolUserProject("workforcePoolUserProject") + .build(); + + assertEquals( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider", + credentials.getAudience()); + assertEquals("subjectTokenType", credentials.getSubjectTokenType()); + assertEquals(STS_URL, credentials.getTokenUrl()); + assertEquals("https://tokeninfo.com", credentials.getTokenInfoUrl()); + assertEquals( + SERVICE_ACCOUNT_IMPERSONATION_URL, credentials.getServiceAccountImpersonationUrl()); + assertEquals(Arrays.asList("scope1", "scope2"), credentials.getScopes()); + assertEquals("projectId", credentials.getQuotaProjectId()); + assertEquals("clientId", credentials.getClientId()); + assertEquals("clientSecret", credentials.getClientSecret()); + assertEquals("workforcePoolUserProject", credentials.getWorkforcePoolUserProject()); + assertNotNull(credentials.getCredentialSource()); + } + + @Test + void constructor_builder_invalidTokenUrl() { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> { - new TestExternalAccountCredentials( - transportFactory, - "audience", - "subjectTokenType", - "tokenUrl", - new TestCredentialSource(new HashMap()), - STS_URL, - /* serviceAccountImpersonationUrl= */ null, - "quotaProjectId", - /* clientId= */ null, - /* clientSecret= */ null, - /* scopes= */ null); + ExternalAccountCredentials.Builder builder = + TestExternalAccountCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl("tokenUrl") + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)); + new TestExternalAccountCredentials(builder); }, "Should have failed since an invalid token URL was passed."); assertEquals("The provided token URL is invalid.", exception.getMessage()); } @Test - void constructor_invalidServiceAccountImpersonationUrl() { + void constructor_builder_invalidServiceAccountImpersonationUrl() { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> { - new TestExternalAccountCredentials( - transportFactory, - "audience", - "subjectTokenType", - "tokenUrl", - new TestCredentialSource(new HashMap<>()), - /* tokenInfoUrl= */ null, - "serviceAccountImpersonationUrl", - "quotaProjectId", - /* clientId= */ null, - /* clientSecret= */ null, - /* scopes= */ null); + ExternalAccountCredentials.Builder builder = + TestExternalAccountCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl("tokenUrl") + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setServiceAccountImpersonationUrl("serviceAccountImpersonationUrl"); + new TestExternalAccountCredentials(builder); }, "Should have failed since an invalid token URL was passed."); assertEquals("The provided token URL is invalid.", exception.getMessage()); } + @Test + void constructor_builderWithInvalidWorkforceAudiences_throws() { + List invalidAudiences = + Arrays.asList( + "", + "//iam.googleapis.com/projects/x23/locations/global/workloadIdentityPools/pool/providers/provider", + "//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider", + "//iam.googleapis.com/locations/global/workforcePools/providers/provider", + "//iam.googleapis.com/locations/global/workforcePools/providers", + "//iam.googleapis.com/locations/global/workforcePools/", + "//iam.googleapis.com/locations//workforcePools/providers", + "//iam.googleapis.com/notlocations/global/workforcePools/providers", + "//iam.googleapis.com/locations/global/workforce/providers"); + + HashMap credentialSource = new HashMap<>(); + credentialSource.put("file", "file"); + for (String audience : invalidAudiences) { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + TestExternalAccountCredentials.newBuilder() + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience(audience) + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setCredentialSource(new TestCredentialSource(credentialSource)) + .build(); + }, + "Exception should be thrown."); + assertEquals( + "The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.", + exception.getMessage()); + } + } + + @Test + void constructor_builderWithEmptyWorkforceUserProjectAndWorkforceAudience() { + HashMap credentialSource = new HashMap<>(); + credentialSource.put("file", "file"); + // No exception should be thrown. + TestExternalAccountCredentials.newBuilder() + .setWorkforcePoolUserProject("") + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setCredentialSource(new TestCredentialSource(credentialSource)) + .build(); + } + @Test void exchangeExternalCredentialForAccessToken() throws IOException { ExternalAccountCredentials credential = @@ -331,6 +420,57 @@ void exchangeExternalCredentialForAccessToken_withInternalOptions() throws IOExc ExternalAccountCredentials credential = ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory); + GenericJson internalOptions = new GenericJson(); + internalOptions.setFactory(OAuth2Utils.JSON_FACTORY); + internalOptions.put("key", "value"); + StsTokenExchangeRequest stsTokenExchangeRequest = + StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType") + .setInternalOptions(internalOptions.toString()) + .build(); + + AccessToken accessToken = + credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest); + + assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); + + // Validate internal options set. + Map query = + TestUtils.parseQuery(transportFactory.transport.getRequest().getContentAsString()); + assertNotNull(query.get("options")); + assertEquals(internalOptions.toString(), query.get("options")); + } + + @Test + void exchangeExternalCredentialForAccessToken_workforceCred_expectUserProjectPassedToSts() + throws IOException { + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson( + buildJsonIdentityPoolWorkforceCredential(), transportFactory); + + StsTokenExchangeRequest stsTokenExchangeRequest = + StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); + + AccessToken accessToken = + credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest); + + assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); + + // Validate internal options set. + Map query = + TestUtils.parseQuery(transportFactory.transport.getRequest().getContentAsString()); + GenericJson internalOptions = new GenericJson(); + internalOptions.setFactory(OAuth2Utils.JSON_FACTORY); + internalOptions.put("userProject", "userProject"); + assertEquals(internalOptions.toString(), query.get("options")); + } + + @Test + void exchangeExternalCredentialForAccessToken_workforceCredWithInternalOptions_expectOverridden() + throws IOException { + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson( + buildJsonIdentityPoolWorkforceCredential(), transportFactory); + GenericJson internalOptions = new GenericJson(); internalOptions.put("key", "value"); StsTokenExchangeRequest stsTokenExchangeRequest = @@ -400,18 +540,15 @@ void exchangeExternalCredentialForAccessToken_throws() throws IOException { @Test void getRequestMetadata_withQuotaProjectId() throws IOException { TestExternalAccountCredentials testCredentials = - new TestExternalAccountCredentials( - transportFactory, - "audience", - "subjectTokenType", - STS_URL, - new TestCredentialSource(new HashMap()), - "tokenInfoUrl", - /* serviceAccountImpersonationUrl= */ null, - "quotaProjectId", - /* clientId= */ null, - /* clientSecret= */ null, - /* scopes= */ null); + (TestExternalAccountCredentials) + TestExternalAccountCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setQuotaProjectId("quotaProjectId") + .build(); Map> requestMetadata = testCredentials.getRequestMetadata(URI.create("http://googleapis.com/foo/bar")); @@ -568,64 +705,27 @@ private GenericJson buildJsonAwsCredential() { } static class TestExternalAccountCredentials extends ExternalAccountCredentials { - static class TestCredentialSource extends ExternalAccountCredentials.CredentialSource { + static class TestCredentialSource extends IdentityPoolCredentials.IdentityPoolCredentialSource { protected TestCredentialSource(Map credentialSourceMap) { super(credentialSourceMap); } } - protected TestExternalAccountCredentials( - 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 scopes) { - super( - transportFactory, - audience, - subjectTokenType, - tokenUrl, - credentialSource, - tokenInfoUrl, - serviceAccountImpersonationUrl, - quotaProjectId, - clientId, - clientSecret, - scopes); + public static Builder newBuilder() { + return new Builder(); + } + + static class Builder extends ExternalAccountCredentials.Builder { + Builder() {} + + @Override + public TestExternalAccountCredentials build() { + return new TestExternalAccountCredentials(this); + } } - protected TestExternalAccountCredentials( - 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 scopes, - @Nullable EnvironmentProvider environmentProvider) { - super( - transportFactory, - audience, - subjectTokenType, - tokenUrl, - credentialSource, - tokenInfoUrl, - serviceAccountImpersonationUrl, - quotaProjectId, - clientId, - clientSecret, - scopes, - environmentProvider); + protected TestExternalAccountCredentials(ExternalAccountCredentials.Builder builder) { + super(builder); } @Override diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index bde045bff..33f55dfc6 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -36,6 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.GenericJson; @@ -378,6 +379,42 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException { transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); } + @Test + void refreshAccessToken_workforceWithServiceAccountImpersonation() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + IdentityPoolCredentials credential = + (IdentityPoolCredentials) + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .setWorkforcePoolUserProject("userProject") + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate internal options set. + Map query = TestUtils.parseQuery(transportFactory.transport.getStsContent()); + + GenericJson expectedInternalOptions = new GenericJson(); + expectedInternalOptions.setFactory(OAuth2Utils.JSON_FACTORY); + expectedInternalOptions.put("userProject", "userProject"); + + assertNotNull(query.get("options")); + assertEquals(expectedInternalOptions.toString(), query.get("options")); + } + @Test void identityPoolCredentialSource_invalidSourceType() { IllegalArgumentException exception = @@ -515,27 +552,23 @@ void builder_invalidWorkforceAudiences_throws() { } @Test - void builder_emptyWorkforceUserProjectWithWorkforceAudience_throws() { - IllegalArgumentException exception = - assertThrows( - IllegalArgumentException.class, - () -> { - IdentityPoolCredentials.newBuilder() - .setWorkforcePoolUserProject("") - .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) - .setAudience( - "//iam.googleapis.com/locations/global/workforcePools/providers/provider") - .setSubjectTokenType("subjectTokenType") - .setTokenUrl(STS_URL) - .setTokenInfoUrl("tokenInfoUrl") - .setCredentialSource(FILE_CREDENTIAL_SOURCE) - .setQuotaProjectId("quotaProjectId") - .build(); - }, - "Exception should be thrown."); - assertEquals( - "The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.", - exception.getMessage()); + void builder_emptyWorkforceUserProjectWithWorkforceAudience() { + // No exception should be thrown. + IdentityPoolCredentials credentials = + (IdentityPoolCredentials) + IdentityPoolCredentials.newBuilder() + .setWorkforcePoolUserProject("") + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(FILE_CREDENTIAL_SOURCE) + .setQuotaProjectId("quotaProjectId") + .build(); + + assertTrue(credentials.isWorkforcePoolConfiguration()); } static InputStream writeIdentityPoolCredentialsStream( diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java index 7e07146cc..43a8dea44 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java @@ -89,6 +89,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport { private MockLowLevelHttpRequest request; private String expireTime; private String metadataServerContentType; + private String stsContent; public void addResponseErrorSequence(IOException... errors) { Collections.addAll(responseErrorSequence, errors); @@ -160,6 +161,10 @@ public LowLevelHttpResponse execute() throws IOException { } if (STS_URL.equals(url)) { Map query = TestUtils.parseQuery(getContentAsString()); + + // Store STS content as multiple calls are made using this transport. + stsContent = getContentAsString(); + assertEquals(EXPECTED_GRANT_TYPE, query.get("grant_type")); assertNotNull(query.get("subject_token_type")); assertNotNull(query.get("subject_token")); @@ -206,6 +211,10 @@ public LowLevelHttpResponse execute() throws IOException { return this.request; } + public String getStsContent() { + return stsContent; + } + public MockLowLevelHttpRequest getRequest() { return request; }