From 31ebe6eaaa96f04fa1afa83c90727ae44f24564a Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Tue, 16 Mar 2021 17:06:03 -0700 Subject: [PATCH] test: adds integration tests for workload identity federation (#581) A setup script workloadidentityfederation-setup.sh is added to make the workload identity pool configuration changes on the current project, if needed. The setup script only needs to be run once on a project (already ran). --- .kokoro/nightly/integration.cfg | 5 + .kokoro/presubmit/integration.cfg | 5 + .../google/auth/oauth2/AwsCredentials.java | 23 +- .../auth/oauth2/EnvironmentProvider.java | 6 + .../oauth2/ExternalAccountCredentials.java | 59 +++- .../auth/oauth2/IdentityPoolCredentials.java | 15 +- .../oauth2/SystemEnvironmentProvider.java | 17 ++ .../auth/oauth2/AwsCredentialsTest.java | 100 ++----- .../ExternalAccountCredentialsTest.java | 32 ++- .../ITWorkloadIdentityFederationTest.java | 272 ++++++++++++++++++ .../oauth2/IdentityPoolCredentialsTest.java | 4 +- .../auth/oauth2/TestEnvironmentProvider.java | 19 ++ oauth2_http/pom.xml | 55 +++- scripts/workloadidentityfederation-setup.sh | 153 ++++++++++ 14 files changed, 663 insertions(+), 102 deletions(-) create mode 100644 oauth2_http/java/com/google/auth/oauth2/EnvironmentProvider.java create mode 100644 oauth2_http/java/com/google/auth/oauth2/SystemEnvironmentProvider.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/TestEnvironmentProvider.java create mode 100755 scripts/workloadidentityfederation-setup.sh diff --git a/.kokoro/nightly/integration.cfg b/.kokoro/nightly/integration.cfg index 0048c8ece..832fcf7cb 100644 --- a/.kokoro/nightly/integration.cfg +++ b/.kokoro/nightly/integration.cfg @@ -35,3 +35,8 @@ env_vars: { key: "SECRET_MANAGER_KEYS" value: "java-it-service-account" } + +env_vars: { + key: "GCS_BUCKET" + value: "byoid-it-bucket" +} diff --git a/.kokoro/presubmit/integration.cfg b/.kokoro/presubmit/integration.cfg index dded67a9d..dd9078cc0 100644 --- a/.kokoro/presubmit/integration.cfg +++ b/.kokoro/presubmit/integration.cfg @@ -31,3 +31,8 @@ env_vars: { key: "SECRET_MANAGER_KEYS" value: "java-it-service-account" } + +env_vars: { + key: "GCS_BUCKET" + value: "byoid-it-bucket" +} diff --git a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index b12d4e1cf..f96cf096a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -117,7 +117,8 @@ static class AwsCredentialSource extends CredentialSource { /** * Internal constructor. See {@link * ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String, - * String, CredentialSource, String, String, String, String, String, Collection)} + * String, CredentialSource, String, String, String, String, String, Collection, + * EnvironmentProvider)} */ AwsCredentials( HttpTransportFactory transportFactory, @@ -130,7 +131,8 @@ static class AwsCredentialSource extends CredentialSource { @Nullable String quotaProjectId, @Nullable String clientId, @Nullable String clientSecret, - @Nullable Collection scopes) { + @Nullable Collection scopes, + @Nullable EnvironmentProvider environmentProvider) { super( transportFactory, audience, @@ -142,7 +144,8 @@ static class AwsCredentialSource extends CredentialSource { quotaProjectId, clientId, clientSecret, - scopes); + scopes, + environmentProvider); this.awsCredentialSource = credentialSource; } @@ -200,7 +203,8 @@ public GoogleCredentials createScoped(Collection newScopes) { getQuotaProjectId(), getClientId(), getClientSecret(), - newScopes); + newScopes, + getEnvironmentProvider()); } private String retrieveResource(String url, String resourceName) throws IOException { @@ -241,7 +245,7 @@ private String buildSubjectToken(AwsRequestSignature signature) private String getAwsRegion() throws IOException { // For AWS Lambda, the region is retrieved through the AWS_REGION environment variable. - String region = getEnv("AWS_REGION"); + String region = getEnvironmentProvider().getEnv("AWS_REGION"); if (region != null) { return region; } @@ -261,9 +265,9 @@ private String getAwsRegion() throws IOException { @VisibleForTesting AwsSecurityCredentials getAwsSecurityCredentials() throws IOException { // Check environment variables for credentials first. - String accessKeyId = getEnv("AWS_ACCESS_KEY_ID"); - String secretAccessKey = getEnv("AWS_SECRET_ACCESS_KEY"); - String token = getEnv("Token"); + String accessKeyId = getEnvironmentProvider().getEnv("AWS_ACCESS_KEY_ID"); + String secretAccessKey = getEnvironmentProvider().getEnv("AWS_SECRET_ACCESS_KEY"); + String token = getEnvironmentProvider().getEnv("Token"); if (accessKeyId != null && secretAccessKey != null) { return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token); } @@ -343,7 +347,8 @@ public AwsCredentials build() { quotaProjectId, clientId, clientSecret, - scopes); + scopes, + environmentProvider); } } } diff --git a/oauth2_http/java/com/google/auth/oauth2/EnvironmentProvider.java b/oauth2_http/java/com/google/auth/oauth2/EnvironmentProvider.java new file mode 100644 index 000000000..5c77ecc65 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/EnvironmentProvider.java @@ -0,0 +1,6 @@ +package com.google.auth.oauth2; + +/** Interface for an environment provider. */ +interface EnvironmentProvider { + String getEnv(String name); +} diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 1373fcc54..9293d5855 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -88,6 +88,8 @@ abstract static class CredentialSource { @Nullable protected final ImpersonatedCredentials impersonatedCredentials; + private EnvironmentProvider environmentProvider; + /** * Constructor with minimum identifying information and custom HTTP transport. * @@ -121,6 +123,41 @@ protected ExternalAccountCredentials( @Nullable String clientId, @Nullable String clientSecret, @Nullable Collection scopes) { + this( + transportFactory, + audience, + subjectTokenType, + tokenUrl, + credentialSource, + tokenInfoUrl, + serviceAccountImpersonationUrl, + quotaProjectId, + clientId, + clientSecret, + scopes, + /* environmentProvider= */ null); + } + + /** + * See {@link ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, + * String, String, CredentialSource, String, String, String, String, String, Collection)} + * + * @param environmentProvider the environment provider. May be null. Defaults to {@link + * SystemEnvironmentProvider}. + */ + protected ExternalAccountCredentials( + 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) { this.transportFactory = MoreObjects.firstNonNull( transportFactory, @@ -137,6 +174,9 @@ protected ExternalAccountCredentials( this.clientSecret = clientSecret; this.scopes = (scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes; + this.environmentProvider = + environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider; + this.impersonatedCredentials = initializeImpersonatedCredentials(); } @@ -251,7 +291,8 @@ static ExternalAccountCredentials fromJson( quotaProjectId, clientId, clientSecret, - /* scopes= */ null); + /* scopes= */ null, + /* environmentProvider= */ null); } return new IdentityPoolCredentials( transportFactory, @@ -264,7 +305,8 @@ static ExternalAccountCredentials fromJson( quotaProjectId, clientId, clientSecret, - /* scopes= */ null); + /* scopes= */ null, + /* environmentProvider= */ null); } private static boolean isAwsCredential(Map credentialSource) { @@ -296,7 +338,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken( } private static String extractTargetPrincipal(String serviceAccountImpersonationUrl) { - // Extract the target principal + // Extract the target principal. int startIndex = serviceAccountImpersonationUrl.lastIndexOf('/'); int endIndex = serviceAccountImpersonationUrl.indexOf(":generateAccessToken"); @@ -364,6 +406,10 @@ public Collection getScopes() { return scopes; } + EnvironmentProvider getEnvironmentProvider() { + return environmentProvider; + } + /** Base builder for external account credentials. */ public abstract static class Builder extends GoogleCredentials.Builder { @@ -372,6 +418,7 @@ public abstract static class Builder extends GoogleCredentials.Builder { protected String tokenUrl; protected String tokenInfoUrl; protected CredentialSource credentialSource; + protected EnvironmentProvider environmentProvider; protected HttpTransportFactory transportFactory; @Nullable protected String serviceAccountImpersonationUrl; @@ -394,6 +441,7 @@ protected Builder(ExternalAccountCredentials credentials) { this.clientId = credentials.clientId; this.clientSecret = credentials.clientSecret; this.scopes = credentials.scopes; + this.environmentProvider = credentials.environmentProvider; } public Builder setAudience(String audience) { @@ -451,6 +499,11 @@ public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { return this; } + Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { + this.environmentProvider = environmentProvider; + return this; + } + public abstract ExternalAccountCredentials build(); } } diff --git a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index a82e3a638..1227f24ed 100644 --- a/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -158,7 +158,8 @@ private boolean hasHeaders() { /** * Internal constructor. See {@link * ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String, - * String, CredentialSource, String, String, String, String, String, Collection)} + * String, CredentialSource, String, String, String, String, String, Collection, + * EnvironmentProvider)} */ IdentityPoolCredentials( HttpTransportFactory transportFactory, @@ -171,7 +172,8 @@ private boolean hasHeaders() { @Nullable String quotaProjectId, @Nullable String clientId, @Nullable String clientSecret, - @Nullable Collection scopes) { + @Nullable Collection scopes, + @Nullable EnvironmentProvider environmentProvider) { super( transportFactory, audience, @@ -183,7 +185,8 @@ private boolean hasHeaders() { quotaProjectId, clientId, clientSecret, - scopes); + scopes, + environmentProvider); this.identityPoolCredentialSource = credentialSource; } @@ -280,7 +283,8 @@ public IdentityPoolCredentials createScoped(Collection newScopes) { getQuotaProjectId(), getClientId(), getClientSecret(), - newScopes); + newScopes, + getEnvironmentProvider()); } public static Builder newBuilder() { @@ -312,7 +316,8 @@ public IdentityPoolCredentials build() { quotaProjectId, clientId, clientSecret, - scopes); + scopes, + environmentProvider); } } } diff --git a/oauth2_http/java/com/google/auth/oauth2/SystemEnvironmentProvider.java b/oauth2_http/java/com/google/auth/oauth2/SystemEnvironmentProvider.java new file mode 100644 index 000000000..d207fed8b --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/SystemEnvironmentProvider.java @@ -0,0 +1,17 @@ +package com.google.auth.oauth2; + +/** Represents the default system environment provider. */ +class SystemEnvironmentProvider implements EnvironmentProvider { + static final SystemEnvironmentProvider INSTANCE = new SystemEnvironmentProvider(); + + private SystemEnvironmentProvider() {} + + @Override + public String getEnv(String name) { + return System.getenv(name); + } + + public static SystemEnvironmentProvider getInstance() { + return INSTANCE; + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index dc86a516f..7537c3098 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -40,7 +40,6 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonParser; import com.google.auth.TestUtils; -import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource; import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory; import java.io.IOException; @@ -48,11 +47,9 @@ import java.net.URI; import java.net.URLDecoder; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.annotation.Nullable; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -264,9 +261,16 @@ public void retrieveSubjectToken_noRegionUrlProvided() { @Test public void getAwsSecurityCredentials_fromEnvironmentVariablesNoToken() throws IOException { - TestAwsCredentials testAwsCredentials = TestAwsCredentials.newBuilder(AWS_CREDENTIAL).build(); - testAwsCredentials.setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId"); - testAwsCredentials.setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + environmentProvider + .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId") + .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); + + AwsCredentials testAwsCredentials = + (AwsCredentials) + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setEnvironmentProvider(environmentProvider) + .build(); AwsSecurityCredentials credentials = testAwsCredentials.getAwsSecurityCredentials(); @@ -277,10 +281,17 @@ public void getAwsSecurityCredentials_fromEnvironmentVariablesNoToken() throws I @Test public void getAwsSecurityCredentials_fromEnvironmentVariablesWithToken() throws IOException { - TestAwsCredentials testAwsCredentials = TestAwsCredentials.newBuilder(AWS_CREDENTIAL).build(); - testAwsCredentials.setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId"); - testAwsCredentials.setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey"); - testAwsCredentials.setEnv("Token", "token"); + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + environmentProvider + .setEnv("AWS_ACCESS_KEY_ID", "awsAccessKeyId") + .setEnv("AWS_SECRET_ACCESS_KEY", "awsSecretAccessKey") + .setEnv("Token", "token"); + + AwsCredentials testAwsCredentials = + (AwsCredentials) + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setEnvironmentProvider(environmentProvider) + .build(); AwsSecurityCredentials credentials = testAwsCredentials.getAwsSecurityCredentials(); @@ -438,6 +449,7 @@ public void builder() { assertEquals(credentials.getClientId(), "clientId"); assertEquals(credentials.getClientSecret(), "clientSecret"); assertEquals(credentials.getScopes(), scopes); + assertEquals(credentials.getEnvironmentProvider(), SystemEnvironmentProvider.getInstance()); } private static AwsCredentialSource buildAwsCredentialSource( @@ -468,72 +480,4 @@ static InputStream writeAwsCredentialsStream(String stsUrl, String regionUrl, St return TestUtils.jsonToInputStream(json); } - - /** Used to test the retrieval of AWS credentials from environment variables. */ - private static class TestAwsCredentials extends AwsCredentials { - - private final Map environmentVariables = new HashMap<>(); - - TestAwsCredentials( - HttpTransportFactory transportFactory, - String audience, - String subjectTokenType, - String tokenUrl, - String tokenInfoUrl, - AwsCredentialSource credentialSource, - @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 TestAwsCredentials.Builder newBuilder(AwsCredentials awsCredentials) { - return new TestAwsCredentials.Builder(awsCredentials); - } - - public static class Builder extends AwsCredentials.Builder { - - private Builder(AwsCredentials credentials) { - super(credentials); - } - - @Override - public TestAwsCredentials build() { - return new TestAwsCredentials( - transportFactory, - audience, - subjectTokenType, - tokenUrl, - tokenInfoUrl, - (AwsCredentialSource) credentialSource, - serviceAccountImpersonationUrl, - quotaProjectId, - clientId, - clientSecret, - scopes); - } - } - - @Override - String getEnv(String name) { - return environmentVariables.get(name); - } - - void setEnv(String name, String value) { - environmentVariables.put(name, value); - } - } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index a9d3ce49b..3065d3420 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -268,8 +268,8 @@ public void getRequestMetadata_withQuotaProjectId() throws IOException { "audience", "subjectTokenType", "tokenUrl", - "tokenInfoUrl", new TestCredentialSource(new HashMap()), + "tokenInfoUrl", /* serviceAccountImpersonationUrl= */ null, "quotaProjectId", /* clientId= */ null, @@ -324,8 +324,8 @@ protected TestExternalAccountCredentials( String audience, String subjectTokenType, String tokenUrl, - String tokenInfoUrl, CredentialSource credentialSource, + @Nullable String tokenInfoUrl, @Nullable String serviceAccountImpersonationUrl, @Nullable String quotaProjectId, @Nullable String clientId, @@ -345,6 +345,34 @@ protected TestExternalAccountCredentials( scopes); } + 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); + } + @Override public AccessToken refreshAccessToken() { return new AccessToken("accessToken", new Date()); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java new file mode 100644 index 000000000..52b6ca269 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java @@ -0,0 +1,272 @@ +/* + * Copyright 2021 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.UrlEncodedContent; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.util.GenericData; +import com.google.auth.http.HttpCredentialsAdapter; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for Workload Identity Federation. + * + *

The only requirements for this test suite to run is to set the environment variable + * GOOGLE_APPLICATION_CREDENTIALS to point to the same service account keys used in the setup script + * (workloadidentityfederation-setup). These tests call GCS to get bucket information. The bucket + * name must be provided through the GCS_BUCKET environment variable. + */ +public final class ITWorkloadIdentityFederationTest { + + // Copy output from workloadidentityfederation-setup. + private static final String AUDIENCE_PREFIX = + "//iam.googleapis.com/projects/1016721519174/locations/global/workloadIdentityPools/pool-1/providers/"; + private static final String AWS_ROLE_NAME = "ci-java-test"; + private static final String AWS_ROLE_ARN = "arn:aws:iam::027472800722:role/ci-java-test"; + + private static final String AWS_AUDIENCE = AUDIENCE_PREFIX + "aws-1"; + private static final String OIDC_AUDIENCE = AUDIENCE_PREFIX + "oidc-1"; + + private String clientEmail; + + @Before + public void setup() throws IOException { + GenericJson keys = getServiceAccountKeyFileAsJson(); + clientEmail = (String) keys.get("client_email"); + } + + /** + * IdentityPoolCredentials (OIDC provider): Uses the service account to generate a Google ID token + * using the iamcredentials generateIdToken API. This will use the service account client ID as + * the sub field of the token. This OIDC token will be used as the external subject token to be + * exchanged for a GCP access token via GCP STS endpoint and then to impersonate the original + * service account key. + */ + @Test + public void identityPoolCredentials() throws IOException { + IdentityPoolCredentials identityPoolCredentials = + (IdentityPoolCredentials) + ExternalAccountCredentials.fromJson( + buildIdentityPoolCredentialConfig(), OAuth2Utils.HTTP_TRANSPORT_FACTORY); + + callGcs(identityPoolCredentials); + } + + /** + * AwsCredentials (AWS provider): Uses the service account keys to generate a Google ID token + * using the iamcredentials generateIdToken API. Exchanges the OIDC ID token for AWS security keys + * using AWS STS AssumeRoleWithWebIdentity API. These values will be set as AWS environment + * variables to simulate an AWS VM. The Auth library can now read these variables and create a + * signed request to AWS GetCallerIdentity. This will be used as the external subject token to be + * exchanged for a GCP access token via GCP STS endpoint and then to impersonate the original + * service account key. + */ + @Test + public void awsCredentials() throws Exception { + String idToken = generateGoogleIdToken(AWS_AUDIENCE); + + String url = + String.format( + "https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity" + + "&Version=2011-06-15&DurationSeconds=3600&RoleSessionName=%s" + + "&RoleArn=%s&WebIdentityToken=%s", + AWS_ROLE_NAME, AWS_ROLE_ARN, idToken); + + HttpRequestFactory requestFactory = new NetHttpTransport().createRequestFactory(); + HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url)); + + JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance()); + request.setParser(parser); + + HttpResponse response = request.execute(); + String rawXml = response.parseAsString(); + + String awsAccessKeyId = getXmlValueByTagName(rawXml, "AccessKeyId"); + String awsSecretAccessKey = getXmlValueByTagName(rawXml, "SecretAccessKey"); + String awsSessionToken = getXmlValueByTagName(rawXml, "SessionToken"); + + AwsCredentials awsCredentialWithoutEnvProvider = + (AwsCredentials) + AwsCredentials.fromJson(buildAwsCredentialConfig(), OAuth2Utils.HTTP_TRANSPORT_FACTORY); + TestEnvironmentProvider testEnvironmentProvider = new TestEnvironmentProvider(); + testEnvironmentProvider + .setEnv("AWS_ACCESS_KEY_ID", awsAccessKeyId) + .setEnv("AWS_SECRET_ACCESS_KEY", awsSecretAccessKey) + .setEnv("Token", awsSessionToken) + .setEnv("AWS_REGION", "us-east-2"); + + AwsCredentials awsCredential = + (AwsCredentials) + AwsCredentials.newBuilder(awsCredentialWithoutEnvProvider) + .setEnvironmentProvider(testEnvironmentProvider) + .build(); + + callGcs(awsCredential); + } + + private GenericJson buildIdentityPoolCredentialConfig() throws IOException { + String idToken = generateGoogleIdToken(OIDC_AUDIENCE); + + File file = + File.createTempFile( + "ITWorkloadIdentityFederation", /* suffix= */ null, /* directory= */ null); + file.deleteOnExit(); + OAuth2Utils.writeInputStreamToFile( + new ByteArrayInputStream(idToken.getBytes(StandardCharsets.UTF_8)), file.getAbsolutePath()); + + GenericJson config = new GenericJson(); + config.put("type", "external_account"); + config.put("audience", OIDC_AUDIENCE); + config.put("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"); + config.put("token_url", "https://sts.googleapis.com/v1beta/token"); + config.put( + "service_account_impersonation_url", + String.format( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", + clientEmail)); + + GenericJson credentialSource = new GenericJson(); + credentialSource.put("file", file.getAbsolutePath()); + config.put("credential_source", credentialSource); + + return config; + } + + private GenericJson buildAwsCredentialConfig() { + GenericJson config = new GenericJson(); + config.put("type", "external_account"); + config.put("audience", AWS_AUDIENCE); + config.put("subject_token_type", "urn:ietf:params:aws:token-type:aws4_request"); + config.put("token_url", "https://sts.googleapis.com/v1beta/token"); + config.put( + "service_account_impersonation_url", + String.format( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", + clientEmail)); + + GenericJson credentialSource = new GenericJson(); + credentialSource.put("environment_id", "aws1"); + credentialSource.put( + "regional_cred_verification_url", + "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"); + config.put("credential_source", credentialSource); + + return config; + } + + private void callGcs(GoogleCredentials credentials) throws IOException { + String bucketName = System.getenv("GCS_BUCKET"); + if (bucketName == null) { + fail("GCS bucket name not set through GCS_BUCKET env variable."); + } + + String url = "https://storage.googleapis.com/storage/v1/b/" + bucketName; + + HttpCredentialsAdapter credentialsAdapter = new HttpCredentialsAdapter(credentials); + HttpRequestFactory requestFactory = + new NetHttpTransport().createRequestFactory(credentialsAdapter); + HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url)); + + JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance()); + request.setParser(parser); + + HttpResponse response = request.execute(); + assertTrue(response.isSuccessStatusCode()); + } + + /** + * Generates a Google ID token using the iamcredentials generateIdToken API. + * https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oidc + */ + private String generateGoogleIdToken(String audience) throws IOException { + GoogleCredentials googleCredentials = + GoogleCredentials.getApplicationDefault() + .createScoped("https://www.googleapis.com/auth/cloud-platform"); + + HttpCredentialsAdapter credentialsAdapter = new HttpCredentialsAdapter(googleCredentials); + HttpRequestFactory requestFactory = + new NetHttpTransport().createRequestFactory(credentialsAdapter); + + String url = + String.format( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken", + clientEmail); + + GenericData data = new GenericData(); + data.set("audience", audience); + data.set("includeEmail", true); + UrlEncodedContent content = new UrlEncodedContent(data); + + HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(url), content); + JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance()); + request.setParser(parser); + + HttpResponse response = request.execute(); + GenericData responseData = response.parseAs(GenericData.class); + return (String) responseData.get("token"); + } + + private GenericJson getServiceAccountKeyFileAsJson() throws IOException { + String credentialsPath = System.getenv(DefaultCredentialsProvider.CREDENTIAL_ENV_VAR); + JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; + JsonObjectParser parser = new JsonObjectParser(jsonFactory); + return parser.parseAndClose( + new FileInputStream(credentialsPath), StandardCharsets.UTF_8, GenericJson.class); + } + + private String getXmlValueByTagName(String rawXml, String tagName) { + int startIndex = rawXml.indexOf("<" + tagName + ">"); + int endIndex = rawXml.indexOf("", startIndex); + + if (startIndex >= 0 && endIndex > startIndex) { + return rawXml.substring(startIndex + tagName.length() + 2, endIndex); + } + return null; + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 4095edcad..871edb043 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -104,8 +104,7 @@ public void createdScoped_clonedCredentialWithAddedScopes() { List newScopes = Arrays.asList("scope1", "scope2"); - IdentityPoolCredentials newCredentials = - (IdentityPoolCredentials) credentials.createScoped(newScopes); + IdentityPoolCredentials newCredentials = credentials.createScoped(newScopes); assertEquals(credentials.getAudience(), newCredentials.getAudience()); assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType()); @@ -444,6 +443,7 @@ public void builder() { assertEquals(credentials.getClientId(), "clientId"); assertEquals(credentials.getClientSecret(), "clientSecret"); assertEquals(credentials.getScopes(), scopes); + assertEquals(credentials.getEnvironmentProvider(), SystemEnvironmentProvider.getInstance()); } static InputStream writeIdentityPoolCredentialsStream( diff --git a/oauth2_http/javatests/com/google/auth/oauth2/TestEnvironmentProvider.java b/oauth2_http/javatests/com/google/auth/oauth2/TestEnvironmentProvider.java new file mode 100644 index 000000000..9e6de1108 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/TestEnvironmentProvider.java @@ -0,0 +1,19 @@ +package com.google.auth.oauth2; + +import java.util.HashMap; +import java.util.Map; + +final class TestEnvironmentProvider implements EnvironmentProvider { + + private final Map environmentVariables = new HashMap<>(); + + @Override + public String getEnv(String name) { + return environmentVariables.get(name); + } + + public TestEnvironmentProvider setEnv(String name, String value) { + environmentVariables.put(name, value); + return this; + } +} diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml index 84f63f4ff..782c0cae0 100644 --- a/oauth2_http/pom.xml +++ b/oauth2_http/pom.xml @@ -51,13 +51,63 @@ org.apache.maven.plugins maven-jar-plugin - + com.google.auth.oauth2 - + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M4 + + 1200 + sponge_log + + **/IT*.java + + + + + org.apache.maven.surefire + surefire-junit47 + 3.0.0-M4 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + **/IT*.java + + sponge_log + + + + org.apache.maven.surefire + surefire-junit47 + 3.0.0-M5 + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + @@ -92,5 +142,4 @@ test - diff --git a/scripts/workloadidentityfederation-setup.sh b/scripts/workloadidentityfederation-setup.sh new file mode 100755 index 000000000..2d7692d6d --- /dev/null +++ b/scripts/workloadidentityfederation-setup.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# Copyright 2021 Google LLC +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google LLC nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This script is used to generate the project configurations needed to +# end-to-end test workload identity pools in the Auth library, specifically +# OIDC-based credentials and AWS credentials. This script only needs to be ran once. +# +# In order to run this script, the GOOGLE_APPLICATION_CREDENTIALS environment +# variable needs to be set to point to a service account key file. +# Additional fields must be provided in this file. +# Detailed instructions are documented below. +# +# GCP project changes: +# -------------------- +# The following IAM roles need to be set on the service account: +# 1. IAM Workload Identity Pool Admin (needed to create resources for workload +# identity pools). +# 2. Security Admin (needed to get and set IAM policies). +# 3. Service Account Token Creator (needed to generate Google ID tokens and +# access tokens). +# +# The following APIs need to be enabled on the project: +# 1. Identity and Access Management (IAM) API. +# 2. IAM Service Account Credentials API. +# 3. Cloud Resource Manager API. +# 4. The API being accessed in the test, eg. DNS. +# +# AWS developer account changes: +# ------------------------------ +# For testing AWS credentials, the following are needed: +# 1. An AWS developer account is needed. The account ID will need to +# be provided in the configuration object below. +# 2. A role for web identity federation. This will also need to be provided +# in the configuration object below. +# - An OIDC Google identity provider needs to be created with the following: +# issuer: accounts.google.com +# audience: Use the client_id of the service account. +# - A role for OIDC web identity federation is needed with the created +# Google provider as a trusted entity: +# "accounts.google.com:aud": "$CLIENT_ID" +# The role creation steps are documented at: +# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html +# +# This script needs to be run once. It will do the following: +# 1. Create a random workload identity pool. +# 2. Create a random OIDC provider in that pool which uses the +# 3. Enable OIDC tokens generated by the current service account to impersonate +# the service account. (Identified by the OIDC token sub field which is the +# service account client ID). +# 4. Create a random AWS provider in that pool which uses the provided AWS +# account ID. +# 5. Enable AWS provider to impersonate the service account. (Principal is +# identified by the AWS role name). +# 6. Print out the STS audience fields associated with the created providers +# and AWS role name/arn after the setup completes successfully so that +# they can be used in the tests. +# +# The same service account used for this setup script should be used for +# the test script. +# +# It is safe to run the setup script again. A new pool is created and new +# audiences are printed. If run multiple times, it is advisable to delete +# unused pools. Note that deleted pools are soft deleted and may remain for +# a while before they are completely deleted. The old pool ID cannot be used +# in the meantime. + +suffix="" + +function generate_random_string () { + local valid_chars=abcdefghijklmnopqrstuvwxyz0123456789 + for i in {1..8} ; do + suffix+="${valid_chars:RANDOM%${#valid_chars}:1}" + done +} + +generate_random_string + +pool_id="pool-"${suffix} +oidc_provider_id="oidc-"${suffix} +aws_provider_id="aws-"${suffix} + +# Fill in. +project_id="" +project_number="" +aws_account_id="" +aws_role_name="" +service_account_email="" +sub=""; # client_id from service account key file + +oidc_aud="//iam.googleapis.com/projects/${project_number}/locations/global/workloadIdentityPools/${pool_id}/providers/${oidc_provider_id}" +aws_aud="//iam.googleapis.com/projects/${project_number}/locations/global/workloadIdentityPools/${pool_id}/providers/${aws_provider_id}" + +gcloud config set project ${project_id} + +# Create the Workload Identity Pool. +gcloud beta iam workload-identity-pools create ${pool_id} \ + --location="global" \ + --description="Test pool" \ + --display-name="Test pool for Java" + +# Create the OIDC Provider. +gcloud beta iam workload-identity-pools providers create-oidc ${oidc_provider_id} \ + --workload-identity-pool=${pool_id} \ + --issuer-uri="https://accounts.google.com" \ + --location="global" \ + --attribute-mapping="google.subject=assertion.sub" + +# Create the AWS Provider. +gcloud beta iam workload-identity-pools providers create-aws ${aws_provider_id} \ + --workload-identity-pool=${pool_id} \ + --account-id=${aws_account_id} \ + --location="global" + +# Give permission to impersonate the service account. +gcloud iam service-accounts add-iam-policy-binding ${service_account_email} \ +--role roles/iam.workloadIdentityUser \ +--member "principal://iam.googleapis.com/projects/${project_number}/locations/global/workloadIdentityPools/${pool_id}/subject/${sub}" + +gcloud iam service-accounts add-iam-policy-binding ${service_account_email} \ + --role roles/iam.workloadIdentityUser \ + --member "principalSet://iam.googleapis.com/projects/${project_number}/locations/global/workloadIdentityPools/${pool_id}/attribute.aws_role/arn:aws:sts::${aws_account_id}:assumed-role/${aws_role_name}" + +echo "OIDC audience:"${oidc_aud} +echo "AWS audience:"${aws_aud} +echo "AWS role name:"${aws_role_name} +echo "AWS role ARN: arn:aws:iam::${aws_account_id}:role/${aws_role_name}"