From f04cf11db1153d9954d0343334de16647733efca Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 25 Mar 2022 17:44:18 +0100 Subject: [PATCH 01/12] chore(deps): update dependency com.google.http-client:google-http-client-bom to v1.41.5 (#896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [com.google.http-client:google-http-client-bom](https://togithub.com/googleapis/google-http-java-client) | `1.41.4` -> `1.41.5` | [![age](https://badges.renovateapi.com/packages/maven/com.google.http-client:google-http-client-bom/1.41.5/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.http-client:google-http-client-bom/1.41.5/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.http-client:google-http-client-bom/1.41.5/compatibility-slim/1.41.4)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.http-client:google-http-client-bom/1.41.5/confidence-slim/1.41.4)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/google-http-java-client ### [`v1.41.5`](https://togithub.com/googleapis/google-http-java-client/blob/HEAD/CHANGELOG.md#​1415-httpsgithubcomgoogleapisgoogle-http-java-clientcomparev1414v1415-2022-03-21) [Compare Source](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.4...v1.41.5)
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-java). --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 18027e07f..049943154 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 - 1.41.4 + 1.41.5 5.8.2 31.0.1-android 2.0.4 From e0d6996ac0db1bf75d92e5aba3eaab512affafe4 Mon Sep 17 00:00:00 2001 From: Navina Ramesh Date: Mon, 28 Mar 2022 10:59:10 -0700 Subject: [PATCH 02/12] feat: Add ability to provide PrivateKey as Pkcs8 encoded string #883 (#889) * feat: Add ability to provide PrivateKey as Pkcs8 encoded string #883 This change adds a new method `setPrivateKeyString` in `ServiceAccountCredentials.Builder` to accept Pkcs8 encoded string representation of private keys. Co-authored-by: Timur Sadykov --- .../com/google/auth/oauth2/ServiceAccountCredentials.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java index 02aff547f..9b9c99c54 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java @@ -1052,6 +1052,11 @@ public Builder setPrivateKey(PrivateKey privateKey) { return this; } + public Builder setPrivateKeyString(String privateKeyPkcs8) throws IOException { + this.privateKey = privateKeyFromPkcs8(privateKeyPkcs8); + return this; + } + public Builder setPrivateKeyId(String privateKeyId) { this.privateKeyId = privateKeyId; return this; From cc670a437adfc436733101af557759c9b4d4bfa7 Mon Sep 17 00:00:00 2001 From: Neenu Shaji Date: Mon, 28 Mar 2022 19:02:21 -0400 Subject: [PATCH 03/12] chore: fix downstream check (#898) --- .github/workflows/downstream.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/downstream.yaml b/.github/workflows/downstream.yaml index 78e1940fa..6985f0ed4 100644 --- a/.github/workflows/downstream.yaml +++ b/.github/workflows/downstream.yaml @@ -134,9 +134,11 @@ jobs: - workflows steps: - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v3 with: + distribution: zulu java-version: ${{matrix.java}} - run: java -version + - run: sudo apt-get update -y - run: sudo apt-get install libxml2-utils - run: .kokoro/downstream-client-library-check.sh google-auth-library-bom ${{matrix.repo}} From 0200dbb05cff06a333879cf99bac64adaada3239 Mon Sep 17 00:00:00 2001 From: Jeff Williams Date: Tue, 29 Mar 2022 15:56:21 -0700 Subject: [PATCH 04/12] fix: update branding in ExternalAccountCredentials (#893) These changes align the Javadoc comments with the branding that Google uses externally: + STS -> Security Token Service + GCP -> Google Cloud + Remove references to a Google-internal token type Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-auth-library-java/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass: Tests are failing, but I don't think that was caused by the changes in this PR - [ ] Code coverage does not decrease (if any source code was changed): n/a - [ ] Appropriate docs were updated (if necessary): n/a --- .../oauth2/ExternalAccountCredentials.java | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 547a04261..4dd02861c 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -58,7 +58,8 @@ /** * Base external account credentials class. * - *

Handles initializing external credentials, calls to STS, and service account impersonation. + *

Handles initializing external credentials, calls to the Security Token Service, and service + * account impersonation. */ public abstract class ExternalAccountCredentials extends GoogleCredentials implements QuotaProjectIdProvider { @@ -89,8 +90,9 @@ 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. + // This is used for Workforce Pools. It is passed to the Security Token Service during token + // exchange in the `options` param and will be embedded in the token by the Security Token + // Service. @Nullable private final String workforcePoolUserProject; protected transient HttpTransportFactory transportFactory; @@ -104,18 +106,17 @@ abstract static class CredentialSource { * 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 - * workload/workforce pool provider - * @param subjectTokenType 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 - * @param tokenUrl the STS token exchange endpoint + * @param audience the Security Token Service audience, which is usually the fully specified + * resource name of the workload/workforce pool provider + * @param subjectTokenType the Security Token Service subject token type based on the OAuth 2.0 + * token exchange spec. Indicates the type of the security token in the credential file + * @param tokenUrl the Security Token Service token exchange endpoint * @param tokenInfoUrl the endpoint used to retrieve account related information. Required for * gCloud session account identification. * @param credentialSource the external credential source * @param serviceAccountImpersonationUrl the URL for the service account impersonation request. - * This is only required for workload identity pools when APIs to be accessed have not - * integrated with UberMint. If this is not available, the STS returned GCP access token is - * directly used. May be null. + * This URL is required for some APIs. If this URL is not available, the access token from the + * Security Token Service is used directly. May be null. * @param quotaProjectId the project used for quota and billing purposes. May be null. * @param clientId client ID of the service account from the console. May be null. * @param clientSecret client secret of the service account from the console. May be null. @@ -395,11 +396,11 @@ private static boolean isAwsCredential(Map credentialSource) { } /** - * Exchanges the external credential for a GCP access token. + * Exchanges the external credential for a Google Cloud access token. * - * @param stsTokenExchangeRequest the STS token exchange request - * @return the access token returned by STS - * @throws OAuthException if the call to STS fails + * @param stsTokenExchangeRequest the Security Token Service token exchange request + * @return the access token returned by the Security Token Service + * @throws OAuthException if the call to the Security Token Service fails */ protected AccessToken exchangeExternalCredentialForAccessToken( StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException { @@ -413,7 +414,8 @@ protected AccessToken exchangeExternalCredentialForAccessToken( 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. + // workforcePoolUserProject must be passed to the Security Token Service via the internal + // options param. if (isWorkforcePoolConfiguration()) { GenericJson options = new GenericJson(); options.setFactory(OAuth2Utils.JSON_FACTORY); @@ -431,7 +433,7 @@ protected AccessToken exchangeExternalCredentialForAccessToken( } /** - * Retrieves the external subject token to be exchanged for a GCP access token. + * Retrieves the external subject token to be exchanged for a Google Cloud access token. * *

Must be implemented by subclasses as the retrieval method is dependent on the credential * source. @@ -603,8 +605,8 @@ public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { } /** - * Sets the STS audience which is usually the fully specified resource name of the - * workload/workforce pool provider. + * Sets the Security Token Service audience, which is usually the fully specified resource name + * of the workload/workforce pool provider. */ public Builder setAudience(String audience) { this.audience = audience; @@ -612,15 +614,15 @@ public Builder setAudience(String audience) { } /** - * 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. + * Sets the Security Token Service 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. */ + /** Sets the Security Token Service token exchange endpoint. */ public Builder setTokenUrl(String tokenUrl) { this.tokenUrl = tokenUrl; return this; @@ -633,9 +635,9 @@ public Builder setCredentialSource(CredentialSource credentialSource) { } /** - * 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. + * Sets the optional URL used for service account impersonation, which is required for some + * APIs. If this URL is not available, the access token from the Security Token Service is used + * directly. */ public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) { this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl; From 305f63e897b8c536c183b7a75585ebdd12aa338c Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Mon, 21 Mar 2022 14:02:50 -0700 Subject: [PATCH 05/12] feat: Adds the ExecutableHandler interface for Pluggable Auth --- .../google/auth/oauth2/ExecutableHandler.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java diff --git a/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java b/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java new file mode 100644 index 000000000..02253f06c --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 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 java.io.IOException; +import java.util.Map; +import javax.annotation.Nullable; + +/** Interface for 3rd party executable handling. */ +interface ExecutableHandler { + + /** Interface for required fields needed to call 3rd party executables. */ + interface ExecutableOptions { + + /** @return The full command (absolute path) used to retrieve 3rd party tokens. */ + String getExecutableCommand(); + + /** + * @return The set of process-local environment variable mappings to be set for the script to + * execute. + */ + Map getEnvironmentMap(); + + /** @return The timeout for waiting for the executable to finish, in milliseconds. */ + int getExecutableTimeoutMs(); + + /** + * @return The output file path which points to the 3rd party credentials generated by the + * executable. + */ + @Nullable + String getOutputFilePath(); + } + + /** + * Handles executing the 3rd party script and parsing the token from the response. + * + * @param options The set executable options for handling the executable. + * @return The 3rd party token. + */ + String retrieveTokenFromExecutable(ExecutableOptions options) throws IOException; +} From 2aa33469d0bee08639d01405b6f75c28f64ac5eb Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Mon, 21 Mar 2022 14:04:02 -0700 Subject: [PATCH 06/12] feat: Adds a Pluggable Auth specific exception --- .../auth/oauth2/PluggableAuthException.java | 48 +++++++++++++ .../oauth2/PluggableAuthExceptionTest.java | 71 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java new file mode 100644 index 000000000..6e84df11c --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 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 com.google.common.base.Preconditions.checkNotNull; + +/** Encapsulates the error response's for 3P executables defined by the executable spec. */ +class PluggableAuthException extends OAuthException { + + PluggableAuthException(String errorCode, String errorDescription) { + super(errorCode, checkNotNull(errorDescription), /* errorUri=*/ null); + } + + /** @return The message with format: Error code {errorCode}: {errorDescription}. */ + @Override + public String getMessage() { + return "Error code " + getErrorCode() + ": " + getErrorDescription(); + } +} diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java new file mode 100644 index 000000000..f924d4137 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** Tests for {@link PluggableAuthException}. */ +class PluggableAuthExceptionTest { + + private static final String MESSAGE_FORMAT = "Error code %s: %s"; + + @Test + void constructor() { + PluggableAuthException e = new PluggableAuthException("errorCode", "errorDescription"); + assertEquals("errorCode", e.getErrorCode()); + assertEquals("errorDescription", e.getErrorDescription()); + } + + @Test + void constructor_nullErrorCode_throws() { + assertThrows( + NullPointerException.class, + () -> new PluggableAuthException(/* errorCode= */ null, "errorDescription")); + } + + @Test + void constructor_nullErrorDescription_throws() { + assertThrows( + NullPointerException.class, + () -> new PluggableAuthException("errorCode", /* errorDescription= */ null)); + } + + @Test + void getMessage() { + PluggableAuthException e = new PluggableAuthException("errorCode", "errorDescription"); + String expectedMessage = String.format("Error code %s: %s", "errorCode", "errorDescription"); + assertEquals(expectedMessage, e.getMessage()); + } +} From c4739c6fc018130ed495ab06d3054ea2ba9df82b Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Mon, 21 Mar 2022 14:04:51 -0700 Subject: [PATCH 07/12] feat: Adds new PluggableAuthCredentials class that plug into ADC --- .../oauth2/ExternalAccountCredentials.java | 40 ++- .../auth/oauth2/PluggableAuthCredentials.java | 266 ++++++++++++++++++ 2 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 4dd02861c..b247fc101 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -39,6 +39,7 @@ import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource; import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource; +import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource; import com.google.common.base.MoreObjects; import java.io.IOException; import java.io.InputStream; @@ -97,7 +98,7 @@ abstract static class CredentialSource { protected transient HttpTransportFactory transportFactory; - @Nullable protected final ImpersonatedCredentials impersonatedCredentials; + @Nullable protected ImpersonatedCredentials impersonatedCredentials; private EnvironmentProvider environmentProvider; @@ -239,7 +240,7 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) this.impersonatedCredentials = initializeImpersonatedCredentials(); } - private ImpersonatedCredentials initializeImpersonatedCredentials() { + protected ImpersonatedCredentials initializeImpersonatedCredentials() { if (serviceAccountImpersonationUrl == null) { return null; } @@ -250,6 +251,11 @@ private ImpersonatedCredentials initializeImpersonatedCredentials() { AwsCredentials.newBuilder((AwsCredentials) this) .setServiceAccountImpersonationUrl(null) .build(); + } else if (this instanceof PluggableAuthCredentials) { + sourceCredentials = + PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this) + .setServiceAccountImpersonationUrl(null) + .build(); } else { sourceCredentials = IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) this) @@ -373,8 +379,20 @@ static ExternalAccountCredentials fromJson( .setClientId(clientId) .setClientSecret(clientSecret) .build(); + } else if (isPluggableAuthCredential(credentialSourceMap)) { + return PluggableAuthCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience(audience) + .setSubjectTokenType(subjectTokenType) + .setTokenUrl(tokenUrl) + .setTokenInfoUrl(tokenInfoUrl) + .setCredentialSource(new PluggableAuthCredentialSource(credentialSourceMap)) + .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl) + .setQuotaProjectId(quotaProjectId) + .setClientId(clientId) + .setClientSecret(clientSecret) + .build(); } - return IdentityPoolCredentials.newBuilder() .setHttpTransportFactory(transportFactory) .setAudience(audience) @@ -390,6 +408,11 @@ static ExternalAccountCredentials fromJson( .build(); } + private static boolean isPluggableAuthCredential(Map credentialSource) { + // Pluggable Auth is enabled via a nested executable field in the credential source. + return credentialSource.containsKey("executable"); + } + private static boolean isAwsCredential(Map credentialSource) { return credentialSource.containsKey("environment_id") && ((String) credentialSource.get("environment_id")).startsWith("aws"); @@ -467,6 +490,15 @@ public String getServiceAccountImpersonationUrl() { return serviceAccountImpersonationUrl; } + /** The service account email to be impersonated, if available. */ + @Nullable + public String getServiceAccountEmail() { + if (serviceAccountImpersonationUrl == null || serviceAccountImpersonationUrl.isEmpty()) { + return null; + } + return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl); + } + @Override @Nullable public String getQuotaProjectId() { @@ -498,7 +530,7 @@ EnvironmentProvider getEnvironmentProvider() { } /** - * Returns whether or not the current configuration is for Workforce Pools (which enable 3p user + * Returns whether the current configuration is for Workforce Pools (which enable 3p user * identities, rather than workloads). */ public boolean isWorkforcePoolConfiguration() { diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java new file mode 100644 index 000000000..ec2f41d79 --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -0,0 +1,266 @@ +/* + * Copyright 2022 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 com.google.auth.oauth2.PluggableAuthHandler.ExecutionOptions; +import com.google.common.annotations.VisibleForTesting; +import java.io.*; +import java.math.BigDecimal; +import java.util.*; +import javax.annotation.Nullable; + +/** + * PluggableAuthCredentials enables the exchange of workload identity pool external credentials for + * Google access tokens by retrieving 3rd party tokens through a user supplied executable. These + * scripts/executables are completely independent of the Google Cloud Auth libraries. These + * credentials plug into ADC and will call the specified executable to retrieve the 3rd party token + * to be exchanged for a Google access token. + * + *

To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable + * must be set to '1'. This is for security reasons. + * + *

Both OIDC and SAML are supported. The executable must adhere to a specific response format + * defined below. + * + *

The executable should print out the 3rd party token to STDOUT in JSON format. This is not + * required when an output_file is specified in the credential source, with the expectation being + * that the output file will contain the JSON response instead. + * + *

OIDC response format { "version": 1, "success": true, "token_type": + * "urn:ietf:params:oauth:token-type:id_token", "id_token": "HEADER.PAYLOAD.SIGNATURE", + * "expiration_time": 1620433341 } + * + *

SAML2 response format { "version": 1, "success": true, "token_type": + * "urn:ietf:params:oauth:token-type:saml2", "saml_response": "...", "expiration_time": 1620433341 } + * + *

Error response format { "version": 1, "success": false, "code": "401", "message": "Error + * message." } + * + *

The auth libraries will populate certain environment variables that will be accessible by the + * executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE, + * GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and + * GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE. + * + *

Please see this repositories README for a complete executable request/response specification. + */ +public class PluggableAuthCredentials extends ExternalAccountCredentials { + + /** + * Encapsulates the credential source portion of the configuration for PluggableAuthCredentials. + * + *

Example credential source for Pluggable Auth credentials: "executable": { "command": + * "/path/to/get/credentials.sh --arg1=value1 --arg2=value2", "timeout_millis": 5000, + * "output_file": "/path/to/generated/cached/credentials" } + * + *

Command is the only required field. If timeout_millis is not specified, the executable will + * default to a 30 second timeout. + */ + static class PluggableAuthCredentialSource extends CredentialSource { + + // The default timeout for waiting for the executable to finish (30 seconds). + private static final int DEFAULT_EXECUTABLE_TIMEOUT_MS = 30 * 1000; + // The minimum timeout for waiting for the executable to finish (5 seconds). + private static final int MINIMUM_EXECUTABLE_TIMEOUT_MS = 5 * 1000; + // The maximum timeout for waiting for the executable to finish (120 seconds). + private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000; + + // Required. The command used to retrieve the 3P token. + private final String executableCommand; + + // Optional. Set to the default timeout when not provided. + private final int executableTimeoutMs; + + // Optional. Provided when the 3P executable caches the response at the specified location. + @Nullable private final String outputFilePath; + + PluggableAuthCredentialSource(Map credentialSourceMap) { + super(credentialSourceMap); + + if (!credentialSourceMap.containsKey("executable")) { + throw new IllegalArgumentException( + "Invalid credential source for PluggableAuth credentials."); + } + + Map executable = (Map) credentialSourceMap.get("executable"); + + // Command is the only required field. + if (!executable.containsKey("command")) { + throw new IllegalArgumentException( + "The PluggableAuthCredentialSource is missing the required 'command' field."); + } + + // Parse the executable timeout. + if (executable.containsKey("timeout_millis")) { + Object timeout = executable.get("timeout_millis"); + if (timeout instanceof BigDecimal) { + executableTimeoutMs = ((BigDecimal) timeout).intValue(); + } else if (executable.get("timeout_millis") instanceof Integer) { + executableTimeoutMs = (int) timeout; + } else { + executableTimeoutMs = Integer.parseInt((String) timeout); + } + } else { + executableTimeoutMs = DEFAULT_EXECUTABLE_TIMEOUT_MS; + } + + // Provided timeout must be between 5s and 120s. + if (executableTimeoutMs < MINIMUM_EXECUTABLE_TIMEOUT_MS + || executableTimeoutMs > MAXIMUM_EXECUTABLE_TIMEOUT_MS) { + throw new IllegalArgumentException( + String.format( + "The executable timeout must be between %s and %s milliseconds.", + MINIMUM_EXECUTABLE_TIMEOUT_MS, MAXIMUM_EXECUTABLE_TIMEOUT_MS)); + } + + executableCommand = (String) executable.get("command"); + outputFilePath = (String) executable.get("output_file"); + } + + String getCommand() { + return executableCommand; + } + + int getTimeoutMs() { + return executableTimeoutMs; + } + + @Nullable + String getOutputFilePath() { + return outputFilePath; + } + } + + private final PluggableAuthCredentialSource config; + + private final ExecutableHandler handler; + + /** Internal constructor. See {@link Builder}. */ + PluggableAuthCredentials(Builder builder) { + super(builder); + this.config = (PluggableAuthCredentialSource) builder.credentialSource; + + if (builder.handler != null) { + handler = builder.handler; + } else { + handler = new PluggableAuthHandler(); + } + + // Re-initialize impersonated credentials as the handler hasn't been set yet when + // this is called in the base class. + this.impersonatedCredentials = initializeImpersonatedCredentials(); + } + + @Override + public AccessToken refreshAccessToken() throws IOException { + String credential = retrieveSubjectToken(); + StsTokenExchangeRequest.Builder stsTokenExchangeRequest = + StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType()) + .setAudience(getAudience()); + + Collection scopes = getScopes(); + if (scopes != null && !scopes.isEmpty()) { + stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); + } + return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build()); + } + + @Override + public String retrieveSubjectToken() throws IOException { + String executableCommand = config.getCommand(); + String outputFilePath = config.getOutputFilePath(); + int executableTimeoutMs = config.getTimeoutMs(); + + Map envMap = new HashMap<>(); + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE", getAudience()); + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE", getSubjectTokenType()); + // Always set to 0 for Workload Identity Federation. + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE", "0"); + if (getServiceAccountEmail() != null) { + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", getServiceAccountEmail()); + } + if (outputFilePath != null && !outputFilePath.isEmpty()) { + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath); + } + + ExecutionOptions options = + ExecutionOptions.newBuilder(envMap, executableCommand) + .setTimeout(executableTimeoutMs) + .setOutputFilePath(outputFilePath) + .build(); + + // Delegate handling of the 3P executable to the handler. + return this.handler.retrieveTokenFromExecutable(options); + } + + /** Clones the PluggableAuthCredentials with the specified scopes. */ + @Override + public PluggableAuthCredentials createScoped(Collection newScopes) { + return new PluggableAuthCredentials( + (PluggableAuthCredentials.Builder) newBuilder(this).setScopes(newScopes)); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(PluggableAuthCredentials pluggableAuthCredentials) { + return new Builder(pluggableAuthCredentials); + } + + @VisibleForTesting + @Nullable + ExecutableHandler getExecutableHandler() { + return this.handler; + } + + public static class Builder extends ExternalAccountCredentials.Builder { + + private ExecutableHandler handler; + + Builder() {} + + Builder(PluggableAuthCredentials credentials) { + super(credentials); + this.handler = credentials.handler; + } + + public Builder setExecutableHandler(ExecutableHandler handler) { + this.handler = handler; + return this; + } + + @Override + public PluggableAuthCredentials build() { + return new PluggableAuthCredentials(this); + } + } +} From 90f0e7d6e1b505b6a79c1b146a655505abd12a91 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Mon, 21 Mar 2022 14:11:57 -0700 Subject: [PATCH 08/12] feat: Adds unit tests for PluggableAuthCredentials and ExternalAccountCredentials --- .../ExternalAccountCredentialsTest.java | 76 +++ .../oauth2/PluggableAuthCredentialsTest.java | 444 ++++++++++++++++++ 2 files changed, 520 insertions(+) create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index fb94bb93d..42413194c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -43,6 +43,7 @@ import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.ExternalAccountCredentialsTest.TestExternalAccountCredentials.TestCredentialSource; +import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; @@ -105,6 +106,16 @@ void fromStream_awsCredentials() throws IOException { assertTrue(credential instanceof AwsCredentials); } + @Test + void fromStream_pluggableAuthCredentials() throws IOException { + GenericJson json = buildJsonPluggableAuthCredential(); + + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json)); + + assertTrue(credential instanceof PluggableAuthCredentials); + } + @Test void fromStream_invalidStream_throws() { GenericJson json = buildJsonAwsCredential(); @@ -203,6 +214,53 @@ void fromJson_awsCredentials() { assertNotNull(credential.getCredentialSource()); } + @Test + void fromJson_pluggableAuthCredentials() { + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson( + buildJsonPluggableAuthCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY); + + assertTrue(credential instanceof PluggableAuthCredentials); + assertEquals("audience", credential.getAudience()); + assertEquals("subjectTokenType", credential.getSubjectTokenType()); + assertEquals(STS_URL, credential.getTokenUrl()); + assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); + assertNotNull(credential.getCredentialSource()); + + PluggableAuthCredentialSource source = + (PluggableAuthCredentialSource) credential.getCredentialSource(); + assertEquals("command", source.getCommand()); + assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s. + assertNull(source.getOutputFilePath()); + } + + @Test + void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() { + GenericJson json = buildJsonPluggableAuthCredential(); + Map credentialSourceMap = (Map) json.get("credential_source"); + // Add optional params to the executable config (timeout, output file path). + Map executableConfig = + (Map) credentialSourceMap.get("executable"); + executableConfig.put("timeout_millis", 5000); + executableConfig.put("output_file", "path/to/output/file"); + + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + + assertTrue(credential instanceof PluggableAuthCredentials); + assertEquals("audience", credential.getAudience()); + assertEquals("subjectTokenType", credential.getSubjectTokenType()); + assertEquals(STS_URL, credential.getTokenUrl()); + assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); + assertNotNull(credential.getCredentialSource()); + + PluggableAuthCredentialSource source = + (PluggableAuthCredentialSource) credential.getCredentialSource(); + assertEquals("command", source.getCommand()); + assertEquals("path/to/output/file", source.getOutputFilePath()); + assertEquals(5000, source.getTimeoutMs()); + } + @Test void fromJson_nullJson_throws() { assertThrows( @@ -704,6 +762,24 @@ private GenericJson buildJsonAwsCredential() { return json; } + private GenericJson buildJsonPluggableAuthCredential() { + GenericJson json = new GenericJson(); + json.put("audience", "audience"); + json.put("subject_token_type", "subjectTokenType"); + json.put("token_url", STS_URL); + json.put("token_info_url", "tokenInfoUrl"); + + Map> credentialSource = new HashMap<>(); + + Map executableConfig = new HashMap<>(); + executableConfig.put("command", "command"); + + credentialSource.put("executable", executableConfig); + json.put("credential_source", credentialSource); + + return json; + } + static class TestExternalAccountCredentials extends ExternalAccountCredentials { static class TestCredentialSource extends IdentityPoolCredentials.IdentityPoolCredentialSource { protected TestCredentialSource(Map credentialSourceMap) { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java new file mode 100644 index 000000000..01185bdb0 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -0,0 +1,444 @@ +/* + * Copyright 2022 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 com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.GenericJson; +import com.google.auth.TestUtils; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions; +import com.google.auth.oauth2.ExternalAccountCredentials.CredentialSource; +import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +/** Tests for {@link PluggableAuthCredentials}. */ +class PluggableAuthCredentialsTest { + // The default timeout for waiting for the executable to finish (30 seconds). + private static final int DEFAULT_EXECUTABLE_TIMEOUT_MS = 30 * 1000; + // The minimum timeout for waiting for the executable to finish (5 seconds). + private static final int MINIMUM_EXECUTABLE_TIMEOUT_MS = 5 * 1000; + // The maximum timeout for waiting for the executable to finish (120 seconds). + private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000; + private static final String STS_URL = "https://sts.googleapis.com"; + + private static final PluggableAuthCredentials CREDENTIAL = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(buildCredentialSource()) + .build(); + + static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory { + + MockExternalAccountCredentialsTransport transport = + new MockExternalAccountCredentialsTransport(); + + @Override + public HttpTransport create() { + return transport; + } + } + + @Test + void retrieveSubjectToken_shouldDelegateToHandler() throws IOException { + PluggableAuthCredentials credential = + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .build(); + String subjectToken = credential.retrieveSubjectToken(); + assertEquals(subjectToken, "pluggableAuthToken"); + } + + @Test + void retrieveSubjectToken_shouldPassAllOptionsToHandler() throws IOException { + String command = "/path/to/executable"; + String timeout = "5000"; + String outputFile = "/path/to/output/file"; + + final ExecutableOptions[] providedOptions = {null}; + ExecutableHandler executableHandler = + options -> { + providedOptions[0] = options; + return "pluggableAuthToken"; + }; + + PluggableAuthCredentials credential = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(executableHandler) + .setCredentialSource(buildCredentialSource(command, timeout, outputFile)) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .build(); + + String subjectToken = credential.retrieveSubjectToken(); + + assertEquals(subjectToken, "pluggableAuthToken"); + + // Validate that the correct options were passed to the executable handler. + ExecutableOptions options = providedOptions[0]; + assertEquals(options.getExecutableCommand(), command); + assertEquals(options.getExecutableTimeoutMs(), Integer.parseInt(timeout)); + assertEquals(options.getOutputFilePath(), outputFile); + + Map envMap = options.getEnvironmentMap(); + assertEquals(envMap.size(), 5); + assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"), credential.getAudience()); + assertEquals( + envMap.get("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"), credential.getSubjectTokenType()); + assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"), "0"); + assertEquals( + envMap.get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"), + credential.getServiceAccountEmail()); + assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"), outputFile); + } + + @Test + void retrieveSubjectToken_shouldPassMinimalOptionsToHandler() throws IOException { + String command = "/path/to/executable"; + + final ExecutableOptions[] providedOptions = {null}; + ExecutableHandler executableHandler = + options -> { + providedOptions[0] = options; + return "pluggableAuthToken"; + }; + + PluggableAuthCredentials credential = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(executableHandler) + .setCredentialSource( + buildCredentialSource(command, /* timeoutMs= */ null, /* outputFile= */ null)) + .build(); + + String subjectToken = credential.retrieveSubjectToken(); + + assertEquals(subjectToken, "pluggableAuthToken"); + + // Validate that the correct options were passed to the executable handler. + ExecutableOptions options = providedOptions[0]; + assertEquals(options.getExecutableCommand(), command); + assertEquals(options.getExecutableTimeoutMs(), DEFAULT_EXECUTABLE_TIMEOUT_MS); + assertNull(options.getOutputFilePath()); + + Map envMap = options.getEnvironmentMap(); + assertEquals(envMap.size(), 3); + assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"), credential.getAudience()); + assertEquals( + envMap.get("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"), credential.getSubjectTokenType()); + assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"), "0"); + assertNull(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL")); + assertNull(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE")); + } + + @Test + void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + + PluggableAuthCredentials credential = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setHttpTransportFactory(transportFactory) + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); + + // Validate that the correct subject token was passed to STS. + Map query = + TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString()); + assertEquals(query.get("subject_token"), "pluggableAuthToken"); + } + + @Test + void refreshAccessToken_withServiceAccountImpersonation() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + + PluggableAuthCredentials credential = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate that the correct subject token was passed to STS. + Map query = + TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString()); + assertEquals(query.get("subject_token"), "pluggableAuthToken"); + } + + @Test + void pluggableAuthCredentialSource_allFields() { + Map source = new HashMap<>(); + Map executable = new HashMap<>(); + source.put("executable", executable); + executable.put("command", "/path/to/executable"); + executable.put("timeout_millis", "10000"); + executable.put("output_file", "/path/to/output/file"); + + PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source); + + assertEquals(credentialSource.getCommand(), "/path/to/executable"); + assertEquals(credentialSource.getTimeoutMs(), 10000); + assertEquals(credentialSource.getOutputFilePath(), "/path/to/output/file"); + } + + @Test + void pluggableAuthCredentialSource_noTimeoutProvided_setToDefault() { + Map source = new HashMap<>(); + Map executable = new HashMap<>(); + source.put("executable", executable); + executable.put("command", "command"); + PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source); + + assertEquals(credentialSource.getCommand(), "command"); + assertEquals(credentialSource.getTimeoutMs(), DEFAULT_EXECUTABLE_TIMEOUT_MS); + assertNull(credentialSource.getOutputFilePath()); + } + + @Test + void pluggableAuthCredentialSource_timeoutProvidedOutOfRange_throws() { + Map source = new HashMap<>(); + Map executable = new HashMap<>(); + source.put("executable", executable); + + executable.put("command", "command"); + + int[] possibleOutOfRangeValues = new int[] {0, 4 * 1000, 121 * 1000}; + + for (int value : possibleOutOfRangeValues) { + executable.put("timeout_millis", value); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + new PluggableAuthCredentialSource(source); + }, + "Exception should be thrown."); + assertEquals( + String.format( + "The executable timeout must be between %s and %s milliseconds.", + MINIMUM_EXECUTABLE_TIMEOUT_MS, MAXIMUM_EXECUTABLE_TIMEOUT_MS), + exception.getMessage()); + } + } + + @Test + void pluggableAuthCredentialSource_validTimeoutProvided() { + Map source = new HashMap<>(); + Map executable = new HashMap<>(); + source.put("executable", executable); + + executable.put("command", "command"); + + Object[] possibleValues = new Object[] {"10000", 10000, BigDecimal.valueOf(10000L)}; + + for (Object value : possibleValues) { + executable.put("timeout_millis", value); + PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source); + + assertEquals(credentialSource.getCommand(), "command"); + assertEquals(credentialSource.getTimeoutMs(), 10000); + assertNull(credentialSource.getOutputFilePath()); + } + } + + @Test + void pluggableAuthCredentialSource_missingExecutableField_throws() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new PluggableAuthCredentialSource(new HashMap<>()), + "Exception should be thrown."); + assertEquals( + "Invalid credential source for PluggableAuth credentials.", exception.getMessage()); + } + + @Test + void pluggableAuthCredentialSource_missingExecutableCommandField_throws() { + Map source = new HashMap<>(); + Map executable = new HashMap<>(); + source.put("executable", executable); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new PluggableAuthCredentialSource(source), + "Exception should be thrown."); + assertEquals( + "The PluggableAuthCredentialSource is missing the required 'command' field.", + exception.getMessage()); + } + + @Test + void builder_allFields() { + List scopes = Arrays.asList("scope1", "scope2"); + + CredentialSource source = buildCredentialSource(); + ExecutableHandler handler = options -> "Token"; + + PluggableAuthCredentials credentials = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder() + .setExecutableHandler(handler) + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .setAudience("audience") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("tokenInfoUrl") + .setCredentialSource(source) + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setScopes(scopes) + .build(); + + assertEquals(credentials.getExecutableHandler(), handler); + assertEquals("audience", credentials.getAudience()); + assertEquals("subjectTokenType", credentials.getSubjectTokenType()); + assertEquals(credentials.getTokenUrl(), STS_URL); + assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl"); + assertEquals( + credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL); + assertEquals(credentials.getCredentialSource(), source); + assertEquals(credentials.getQuotaProjectId(), "quotaProjectId"); + assertEquals(credentials.getClientId(), "clientId"); + assertEquals(credentials.getClientSecret(), "clientSecret"); + assertEquals(credentials.getScopes(), scopes); + assertEquals(credentials.getEnvironmentProvider(), SystemEnvironmentProvider.getInstance()); + } + + @Test + void createdScoped_clonedCredentialWithAddedScopes() { + PluggableAuthCredentials credentials = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setQuotaProjectId("quotaProjectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .build(); + + List newScopes = Arrays.asList("scope1", "scope2"); + + PluggableAuthCredentials newCredentials = credentials.createScoped(newScopes); + + assertEquals(credentials.getAudience(), newCredentials.getAudience()); + assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType()); + assertEquals(credentials.getTokenUrl(), newCredentials.getTokenUrl()); + assertEquals(credentials.getTokenInfoUrl(), newCredentials.getTokenInfoUrl()); + assertEquals( + credentials.getServiceAccountImpersonationUrl(), + newCredentials.getServiceAccountImpersonationUrl()); + assertEquals(credentials.getCredentialSource(), newCredentials.getCredentialSource()); + assertEquals(newScopes, newCredentials.getScopes()); + assertEquals(credentials.getQuotaProjectId(), newCredentials.getQuotaProjectId()); + assertEquals(credentials.getClientId(), newCredentials.getClientId()); + assertEquals(credentials.getClientSecret(), newCredentials.getClientSecret()); + assertEquals(credentials.getExecutableHandler(), newCredentials.getExecutableHandler()); + } + + private static CredentialSource buildCredentialSource() { + return buildCredentialSource("command", null, null); + } + + private static CredentialSource buildCredentialSource( + String command, @Nullable String timeoutMs, @Nullable String outputFile) { + Map source = new HashMap<>(); + Map executable = new HashMap<>(); + source.put("executable", executable); + executable.put("command", command); + if (timeoutMs != null) { + executable.put("timeout_millis", timeoutMs); + } + if (outputFile != null) { + executable.put("output_file", outputFile); + } + + return new PluggableAuthCredentialSource(source); + } + + static InputStream writeCredentialsStream(String tokenUrl) throws IOException { + GenericJson json = new GenericJson(); + json.put("audience", "audience"); + json.put("subject_token_type", "subjectTokenType"); + json.put("token_url", tokenUrl); + json.put("token_info_url", "tokenInfoUrl"); + json.put("type", ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE); + + GenericJson credentialSource = new GenericJson(); + GenericJson executable = new GenericJson(); + executable.put("command", "/path/to/executable"); + credentialSource.put("executable", executable); + + json.put("credential_source", credentialSource); + return TestUtils.jsonToInputStream(json); + } +} From d355e37be4bedc6074242fad404c2c61259cf8c4 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Mon, 21 Mar 2022 14:49:40 -0700 Subject: [PATCH 09/12] Add units tests for GoogleCredentials --- .../auth/oauth2/PluggableAuthCredentials.java | 34 +++++++++++++++---- .../auth/oauth2/GoogleCredentialsTest.java | 23 +++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index ec2f41d79..bb596632e 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -31,7 +31,7 @@ package com.google.auth.oauth2; -import com.google.auth.oauth2.PluggableAuthHandler.ExecutionOptions; +import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions; import com.google.common.annotations.VisibleForTesting; import java.io.*; import java.math.BigDecimal; @@ -171,7 +171,8 @@ String getOutputFilePath() { if (builder.handler != null) { handler = builder.handler; } else { - handler = new PluggableAuthHandler(); + // TODO(lsirac): Initialize handler. + handler = null; } // Re-initialize impersonated credentials as the handler hasn't been set yet when @@ -211,11 +212,30 @@ public String retrieveSubjectToken() throws IOException { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath); } - ExecutionOptions options = - ExecutionOptions.newBuilder(envMap, executableCommand) - .setTimeout(executableTimeoutMs) - .setOutputFilePath(outputFilePath) - .build(); + // TODO(lsirac): replace with actual ExecutableOptions implementation. + ExecutableOptions options = + new ExecutableOptions() { + @Override + public String getExecutableCommand() { + return executableCommand; + } + + @Override + public Map getEnvironmentMap() { + return envMap; + } + + @Override + public int getExecutableTimeoutMs() { + return executableTimeoutMs; + } + + @Nullable + @Override + public String getOutputFilePath() { + return outputFilePath; + } + }; // Delegate handling of the 3P executable to the handler. return this.handler.retrieveTokenFromExecutable(options); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java index 028f9235e..f5bd15d2d 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java @@ -260,6 +260,29 @@ void fromStream_awsCredentials_providesToken() throws IOException { TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken()); } + @Test + void fromStream_pluggableAuthCredentials_providesToken() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + InputStream stream = + PluggableAuthCredentialsTest.writeCredentialsStream(transportFactory.transport.getStsUrl()); + + GoogleCredentials credentials = GoogleCredentials.fromStream(stream, transportFactory); + + assertNotNull(credentials); + + // Create copy with mock executable handler. + PluggableAuthCredentials copy = + PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) credentials) + .setExecutableHandler(options -> "pluggableAuthToken") + .build(); + + copy = copy.createScoped(SCOPES); + Map> metadata = copy.getRequestMetadata(CALL_URI); + TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken()); + } + @Test void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOException { MockTokenServerTransportFactory transportFactoryForSource = From 80f91b3a1725f7d312ee856f05a2eeb2aae3638b Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Tue, 22 Mar 2022 17:17:08 -0700 Subject: [PATCH 10/12] fix: update javadoc/comments --- .../auth/oauth2/PluggableAuthCredentials.java | 61 ++++++++++++++----- .../auth/oauth2/PluggableAuthException.java | 2 +- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index bb596632e..3212ef38f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -55,34 +55,62 @@ * required when an output_file is specified in the credential source, with the expectation being * that the output file will contain the JSON response instead. * - *

OIDC response format { "version": 1, "success": true, "token_type": - * "urn:ietf:params:oauth:token-type:id_token", "id_token": "HEADER.PAYLOAD.SIGNATURE", - * "expiration_time": 1620433341 } + *

+ * OIDC response sample:
+ * {
+ *   "version": 1,
+ *   "success": true,
+ *   "token_type": "urn:ietf:params:oauth:token-type:id_token",
+ *   "id_token": "HEADER.PAYLOAD.SIGNATURE",
+ *   "expiration_time": 1620433341
+ * }
  *
- * 

SAML2 response format { "version": 1, "success": true, "token_type": - * "urn:ietf:params:oauth:token-type:saml2", "saml_response": "...", "expiration_time": 1620433341 } + * SAML2 response sample: + * { + * "version": 1, + * "success": true, + * "token_type": "urn:ietf:params:oauth:token-type:saml2", + * "saml_response": "...", + * "expiration_time": 1620433341 + * } * - *

Error response format { "version": 1, "success": false, "code": "401", "message": "Error - * message." } + * Error response sample: + * { + * "version": 1, + * "success": false, + * "code": "401", + * "message": "Error message." + * } * - *

The auth libraries will populate certain environment variables that will be accessible by the + * The auth libraries will populate certain environment variables that will be accessible by the * executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE, * GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and * GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE. * *

Please see this repositories README for a complete executable request/response specification. + *

*/ public class PluggableAuthCredentials extends ExternalAccountCredentials { /** * Encapsulates the credential source portion of the configuration for PluggableAuthCredentials. * - *

Example credential source for Pluggable Auth credentials: "executable": { "command": - * "/path/to/get/credentials.sh --arg1=value1 --arg2=value2", "timeout_millis": 5000, - * "output_file": "/path/to/generated/cached/credentials" } - * - *

Command is the only required field. If timeout_millis is not specified, the executable will + *

Command is the only required field. If timeout_millis is not specified, the library will * default to a 30 second timeout. + * + *

+   * Sample credential source for Pluggable Auth credentials:
+   * {
+   *   ...
+   *   "credential_source": {
+   *     "executable": {
+   *       "command": "/path/to/get/credentials.sh --arg1=value1 --arg2=value2",
+   *       "timeout_millis": 5000,
+   *       "output_file": "/path/to/generated/cached/credentials"
+   *     }
+   *   }
+   * }
+   * 
*/ static class PluggableAuthCredentialSource extends CredentialSource { @@ -93,13 +121,14 @@ static class PluggableAuthCredentialSource extends CredentialSource { // The maximum timeout for waiting for the executable to finish (120 seconds). private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000; - // Required. The command used to retrieve the 3P token. + // Required. The command used to retrieve the 3rd party token. private final String executableCommand; // Optional. Set to the default timeout when not provided. private final int executableTimeoutMs; - // Optional. Provided when the 3P executable caches the response at the specified location. + // Optional. Provided when the 3rd party executable caches the response at the specified + // location. @Nullable private final String outputFilePath; PluggableAuthCredentialSource(Map credentialSourceMap) { @@ -237,7 +266,7 @@ public String getOutputFilePath() { } }; - // Delegate handling of the 3P executable to the handler. + // Delegate handling of the executable to the handler. return this.handler.retrieveTokenFromExecutable(options); } diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java index 6e84df11c..27b0092f6 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java @@ -33,7 +33,7 @@ import static com.google.common.base.Preconditions.checkNotNull; -/** Encapsulates the error response's for 3P executables defined by the executable spec. */ +/** Encapsulates the error response's for 3rd party executables defined by the executable spec. */ class PluggableAuthException extends OAuthException { PluggableAuthException(String errorCode, String errorDescription) { From b1e72843b91eb709305b5c9068c43781e7676262 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Wed, 23 Mar 2022 15:14:33 -0700 Subject: [PATCH 11/12] fix: A concrete ExecutableOptions implementation is not needed --- .../java/com/google/auth/oauth2/PluggableAuthCredentials.java | 1 - 1 file changed, 1 deletion(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 3212ef38f..ec464f4f8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -241,7 +241,6 @@ public String retrieveSubjectToken() throws IOException { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath); } - // TODO(lsirac): replace with actual ExecutableOptions implementation. ExecutableOptions options = new ExecutableOptions() { @Override From 175d959987ff8689b62bc00f91fb86871df95ab2 Mon Sep 17 00:00:00 2001 From: Leonardo Siracusa Date: Wed, 30 Mar 2022 15:22:16 -0700 Subject: [PATCH 12/12] review: javadoc changes + constants --- .../google/auth/oauth2/ExecutableHandler.java | 20 +++++++----------- .../oauth2/ExternalAccountCredentials.java | 3 ++- .../auth/oauth2/PluggableAuthCredentials.java | 21 ++++++++++++------- .../auth/oauth2/PluggableAuthException.java | 2 +- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java b/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java index 02253f06c..a052f2a5b 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java @@ -35,27 +35,23 @@ import java.util.Map; import javax.annotation.Nullable; -/** Interface for 3rd party executable handling. */ +/** An interface for 3rd party executable handling. */ interface ExecutableHandler { - /** Interface for required fields needed to call 3rd party executables. */ + /** An interface for required fields needed to call 3rd party executables. */ interface ExecutableOptions { - /** @return The full command (absolute path) used to retrieve 3rd party tokens. */ + /** An absolute path to the command used to retrieve 3rd party tokens. */ String getExecutableCommand(); - /** - * @return The set of process-local environment variable mappings to be set for the script to - * execute. - */ + /** A set of process-local environment variable mappings to be set for the script to execute. */ Map getEnvironmentMap(); - /** @return The timeout for waiting for the executable to finish, in milliseconds. */ + /** A timeout for waiting for the executable to finish, in milliseconds. */ int getExecutableTimeoutMs(); /** - * @return The output file path which points to the 3rd party credentials generated by the - * executable. + * An output file path which points to the 3rd party credentials generated by the executable. */ @Nullable String getOutputFilePath(); @@ -64,8 +60,8 @@ interface ExecutableOptions { /** * Handles executing the 3rd party script and parsing the token from the response. * - * @param options The set executable options for handling the executable. - * @return The 3rd party token. + * @param options A set executable options for handling the executable. + * @return A 3rd party token. */ String retrieveTokenFromExecutable(ExecutableOptions options) throws IOException; } diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index b247fc101..b88f98bc5 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -77,6 +77,7 @@ abstract static class CredentialSource { "https://www.googleapis.com/auth/cloud-platform"; static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account"; + static final String EXECUTABLE_SOURCE_KEY = "executable"; private final String transportFactoryClassName; private final String audience; @@ -410,7 +411,7 @@ static ExternalAccountCredentials fromJson( private static boolean isPluggableAuthCredential(Map credentialSource) { // Pluggable Auth is enabled via a nested executable field in the credential source. - return credentialSource.containsKey("executable"); + return credentialSource.containsKey(EXECUTABLE_SOURCE_KEY); } private static boolean isAwsCredential(Map credentialSource) { diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index ec464f4f8..83c63a5d8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -121,6 +121,10 @@ static class PluggableAuthCredentialSource extends CredentialSource { // The maximum timeout for waiting for the executable to finish (120 seconds). private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000; + private static final String COMMAND_KEY = "command"; + private static final String TIMEOUT_MILLIS_KEY = "timeout_millis"; + private static final String OUTPUT_FILE_KEY = "output_file"; + // Required. The command used to retrieve the 3rd party token. private final String executableCommand; @@ -134,25 +138,26 @@ static class PluggableAuthCredentialSource extends CredentialSource { PluggableAuthCredentialSource(Map credentialSourceMap) { super(credentialSourceMap); - if (!credentialSourceMap.containsKey("executable")) { + if (!credentialSourceMap.containsKey(EXECUTABLE_SOURCE_KEY)) { throw new IllegalArgumentException( "Invalid credential source for PluggableAuth credentials."); } - Map executable = (Map) credentialSourceMap.get("executable"); + Map executable = + (Map) credentialSourceMap.get(EXECUTABLE_SOURCE_KEY); // Command is the only required field. - if (!executable.containsKey("command")) { + if (!executable.containsKey(COMMAND_KEY)) { throw new IllegalArgumentException( "The PluggableAuthCredentialSource is missing the required 'command' field."); } // Parse the executable timeout. - if (executable.containsKey("timeout_millis")) { - Object timeout = executable.get("timeout_millis"); + if (executable.containsKey(TIMEOUT_MILLIS_KEY)) { + Object timeout = executable.get(TIMEOUT_MILLIS_KEY); if (timeout instanceof BigDecimal) { executableTimeoutMs = ((BigDecimal) timeout).intValue(); - } else if (executable.get("timeout_millis") instanceof Integer) { + } else if (executable.get(TIMEOUT_MILLIS_KEY) instanceof Integer) { executableTimeoutMs = (int) timeout; } else { executableTimeoutMs = Integer.parseInt((String) timeout); @@ -170,8 +175,8 @@ static class PluggableAuthCredentialSource extends CredentialSource { MINIMUM_EXECUTABLE_TIMEOUT_MS, MAXIMUM_EXECUTABLE_TIMEOUT_MS)); } - executableCommand = (String) executable.get("command"); - outputFilePath = (String) executable.get("output_file"); + executableCommand = (String) executable.get(COMMAND_KEY); + outputFilePath = (String) executable.get(OUTPUT_FILE_KEY); } String getCommand() { diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java index 27b0092f6..894b324a9 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java @@ -40,7 +40,7 @@ class PluggableAuthException extends OAuthException { super(errorCode, checkNotNull(errorDescription), /* errorUri=*/ null); } - /** @return The message with format: Error code {errorCode}: {errorDescription}. */ + /** The message with format: Error code {errorCode}: {errorDescription}. */ @Override public String getMessage() { return "Error code " + getErrorCode() + ": " + getErrorDescription();