-
Notifications
You must be signed in to change notification settings - Fork 259
feat: Support querying S2A Addresses from MDS #1400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c96cb4a
f90be0b
0c64a0a
993663d
0f96e86
3d68cef
6d75a4e
d932e0c
6aa071b
ddac7aa
2c26736
67f9462
36d4cd1
fc2b246
359fd43
bce602e
32caef5
b82790a
05aa9cc
1466f0d
be1cfd2
c89b56c
544d9d1
c3ede1d
8238d50
36ab0a9
36a0ac7
ae545c8
47b3f2e
fb577a1
1f333b4
0bbd320
0e6f5ce
e786886
12b248d
4d05638
7447f0b
ed681f5
20825f7
594df7b
16fd964
1e6c058
934679c
257ed12
0e1631a
6644d50
8ca8d69
699bed7
8e5ccb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| /* | ||
| * Copyright 2024, Google Inc. All rights reserved. | ||
| * | ||
| * 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 Inc. 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.api.client.http.GenericUrl; | ||
| import com.google.api.client.http.HttpBackOffIOExceptionHandler; | ||
| import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; | ||
| import com.google.api.client.http.HttpRequest; | ||
| import com.google.api.client.http.HttpResponse; | ||
| import com.google.api.client.json.JsonObjectParser; | ||
| import com.google.api.client.util.ExponentialBackOff; | ||
| import com.google.api.client.util.GenericData; | ||
| import com.google.auth.http.HttpTransportFactory; | ||
| import com.google.common.collect.Iterables; | ||
| import com.google.errorprone.annotations.CanIgnoreReturnValue; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.util.Arrays; | ||
| import java.util.HashSet; | ||
| import java.util.ServiceLoader; | ||
| import java.util.Set; | ||
| import javax.annotation.concurrent.ThreadSafe; | ||
|
|
||
| /** | ||
| * Utilities to fetch the S2A (Secure Session Agent) address from the mTLS configuration. | ||
| * | ||
| * <p>mTLS configuration is queried from the MDS MTLS Autoconfiguration endpoint. See | ||
| * https://google.aip.dev/auth/4115 for details. | ||
| * | ||
| * <p>This is an experimental utility. | ||
| */ | ||
| @ThreadSafe | ||
| public final class S2A { | ||
| static final String S2A_PLAINTEXT_ADDRESS_JSON_KEY = "plaintext_address"; | ||
| static final String S2A_MTLS_ADDRESS_JSON_KEY = "mtls_address"; | ||
| static final String S2A_CONFIG_ENDPOINT_POSTFIX = | ||
| "/computeMetadata/v1/instance/platform-security/auto-mtls-configuration"; | ||
|
|
||
| static final String METADATA_FLAVOR = "Metadata-Flavor"; | ||
| static final String GOOGLE = "Google"; | ||
| private static final Set<Integer> RETRYABLE_STATUS_CODES = | ||
| new HashSet<>(Arrays.asList(500, 502, 503)); | ||
| private static final String PARSE_ERROR_S2A = "Error parsing S2A Config from MDS JSON response."; | ||
| private static final String MDS_MTLS_ENDPOINT = | ||
| ComputeEngineCredentials.getMetadataServerUrl() + S2A_CONFIG_ENDPOINT_POSTFIX; | ||
|
|
||
| private S2AConfig config; | ||
|
|
||
| private transient HttpTransportFactory transportFactory; | ||
|
|
||
| S2A(S2A.Builder builder) { | ||
| this.transportFactory = builder.getHttpTransportFactory(); | ||
| this.config = getS2AConfigFromMDS(); | ||
| } | ||
|
|
||
| /** @return the mTLS S2A Address from the mTLS config. */ | ||
| public String getMtlsS2AAddress() { | ||
| return config.getMtlsAddress(); | ||
| } | ||
|
|
||
| /** @return the plaintext S2A Address from the mTLS config. */ | ||
| public String getPlaintextS2AAddress() { | ||
| return config.getPlaintextAddress(); | ||
| } | ||
|
|
||
| public static Builder newBuilder() { | ||
| return new Builder(); | ||
| } | ||
|
|
||
| public static class Builder { | ||
| private HttpTransportFactory transportFactory; | ||
|
|
||
| protected Builder() {} | ||
|
|
||
| @CanIgnoreReturnValue | ||
| public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { | ||
| this.transportFactory = transportFactory; | ||
| return this; | ||
| } | ||
|
|
||
| public HttpTransportFactory getHttpTransportFactory() { | ||
| return this.transportFactory; | ||
| } | ||
|
|
||
| public S2A build() { | ||
| return new S2A(this); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Queries the MDS mTLS Autoconfiguration endpoint and returns the {@link S2AConfig}. | ||
| * | ||
| * <p>Returns {@link S2AConfig}. If S2A is not running, or if any error occurs when making the | ||
| * request to MDS / processing the response, {@link S2AConfig} will be populated with empty | ||
| * addresses. | ||
| * | ||
| * <p>Users are expected to try to fetch the mTLS-S2A address first (via {@link | ||
| * getMtlsS2AAddress}). If it is empty or they have some problem loading the mTLS-MDS credentials, | ||
| * they should then fallback to fetching the plaintext-S2A address (via {@link | ||
| * getPlaintextS2AAddress}). If the plaintext-S2A address is empty it means that an error occurred | ||
| * when talking to the MDS / processing the response or that S2A is not running in the | ||
| * environment; in either case this indicates S2A shouldn't be used. | ||
| * | ||
| * @return the {@link S2AConfig}. | ||
| */ | ||
| private S2AConfig getS2AConfigFromMDS() { | ||
| if (transportFactory == null) { | ||
| transportFactory = | ||
| Iterables.getFirst( | ||
| ServiceLoader.load(HttpTransportFactory.class), OAuth2Utils.HTTP_TRANSPORT_FACTORY); | ||
| } | ||
|
|
||
| HttpRequest request = null; | ||
| GenericUrl genericUrl = new GenericUrl(MDS_MTLS_ENDPOINT); | ||
| try { | ||
| request = transportFactory.create().createRequestFactory().buildGetRequest(genericUrl); | ||
| } catch (IOException ignore) { | ||
| /* | ||
| * Return empty addresses in {@link S2AConfig} if error building the GET request. | ||
| */ | ||
| return S2AConfig.createBuilder().build(); | ||
| } | ||
|
|
||
| request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY)); | ||
| request.getHeaders().set(METADATA_FLAVOR, GOOGLE); | ||
| request.setThrowExceptionOnExecuteError(false); | ||
| request.setNumberOfRetries(OAuth2Utils.DEFAULT_NUMBER_OF_RETRIES); | ||
|
|
||
| ExponentialBackOff backoff = | ||
| new ExponentialBackOff.Builder() | ||
| .setInitialIntervalMillis(OAuth2Utils.INITIAL_RETRY_INTERVAL_MILLIS) | ||
| .setRandomizationFactor(OAuth2Utils.RETRY_RANDOMIZATION_FACTOR) | ||
| .setMultiplier(OAuth2Utils.RETRY_MULTIPLIER) | ||
| .build(); | ||
|
|
||
| // Retry on 5xx status codes. | ||
| request.setUnsuccessfulResponseHandler( | ||
| new HttpBackOffUnsuccessfulResponseHandler(backoff) | ||
| .setBackOffRequired( | ||
| response -> RETRYABLE_STATUS_CODES.contains(response.getStatusCode()))); | ||
| request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(backoff)); | ||
|
|
||
| GenericData responseData = null; | ||
| try { | ||
| HttpResponse response = request.execute(); | ||
| InputStream content = response.getContent(); | ||
|
Comment on lines
+173
to
+174
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe do try/catch block around these to be more specific about the exceptions catching? Nested try/catch blocks are a bit hard to read.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I think I was the one that suggested this, but am open to having the try-catch blocks be more specific or a different implementation. I suggested just have one try-catch block since this code doesn't catch or return the error message back to the user. It just catches any IO error and returns an empty S2AConfig back. Unfortunately, this doesn't provide any useful feedback to the user. Given that this is how it behaves already in Go, I thought it would just be simpler to have a catch-all (one big try block) for this logic. Ideally, we would have more specific try-catch blocks which could tell the user exactly what went wrong (i.e. failed to build the request because x, or the request failed because of y). I believe there are two spots where we potentially would need to add try-catches:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a few places in this block that throw an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lqiu96 , I think my changes and your comment happened at same time. Reading through your comment, the changes I just push match your suggestion. I removed the big try/catch and just did 2 smaller ones around building the request and executing it. @zhumin8 , @lqiu96: I am fine with either option -- let me know if you prefer reverting to the 1 big/try catch block, or if you prefer the current state which has no nesting, but a few smaller try/catch blocks
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am fine with splitting it up to reduce nesting. |
||
| if (content == null) { | ||
| return S2AConfig.createBuilder().build(); | ||
| } | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| responseData = response.parseAs(GenericData.class); | ||
| } catch (IOException ignore) { | ||
| /* | ||
| * Return empty addresses in {@link S2AConfig} once all retries have been exhausted. | ||
| */ | ||
| return S2AConfig.createBuilder().build(); | ||
| } | ||
|
|
||
| String plaintextS2AAddress = ""; | ||
| String mtlsS2AAddress = ""; | ||
| try { | ||
| plaintextS2AAddress = | ||
| OAuth2Utils.validateString(responseData, S2A_PLAINTEXT_ADDRESS_JSON_KEY, PARSE_ERROR_S2A); | ||
| } catch (IOException ignore) { | ||
| /* | ||
| * Do not throw error because of parsing error, just leave the address as empty in {@link S2AConfig}. | ||
| */ | ||
| } | ||
| try { | ||
| mtlsS2AAddress = | ||
| OAuth2Utils.validateString(responseData, S2A_MTLS_ADDRESS_JSON_KEY, PARSE_ERROR_S2A); | ||
| } catch (IOException ignore) { | ||
| /* | ||
| * Do not throw error because of parsing error, just leave the address as empty in {@link S2AConfig}. | ||
| */ | ||
| } | ||
|
|
||
| return S2AConfig.createBuilder() | ||
| .setPlaintextAddress(plaintextS2AAddress) | ||
| .setMtlsAddress(mtlsS2AAddress) | ||
| .build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* | ||
| * Copyright 2024, Google Inc. All rights reserved. | ||
| * | ||
| * 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 Inc. 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; | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| import com.google.errorprone.annotations.CanIgnoreReturnValue; | ||
|
|
||
| /** Holds an mTLS configuration (consists of address of S2A) retrieved from the Metadata Server. */ | ||
| final class S2AConfig { | ||
| // plaintextAddress is the plaintext address to reach the S2A. | ||
| private final String plaintextAddress; | ||
|
|
||
| // mtlsAddress is the mTLS address to reach the S2A. | ||
| private final String mtlsAddress; | ||
|
|
||
| public static Builder createBuilder() { | ||
| return new Builder(); | ||
| } | ||
|
|
||
| /** @return the plaintext S2A Address. */ | ||
| public String getPlaintextAddress() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like Javadocs for all these public things, pointing to public docs for MDS.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't have this documented in public MDS docs (e.g. https://cloud.google.com/compute/docs/metadata/predefined-metadata-keys). We do have an AIP: https://google.aip.dev/auth/4115 which discusses this autconfig endpoint and how it fits in the mTLS via S2A + bound tokens story. WDYT about 934679c? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please include the documentation for each public method here. The AIP isn't a great landing place -- can you please look into improving the public MDS docs at least with a bug to the metadata service team?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 8ca8d69 The AIP isn't a great landing place -- can you please look into improving the public MDS docs at least with a bug to the metadata service team?
|
||
| return plaintextAddress; | ||
| } | ||
|
|
||
| /** @return the mTLS S2A Address. */ | ||
| public String getMtlsAddress() { | ||
| return mtlsAddress; | ||
| } | ||
|
|
||
| public static final class Builder { | ||
| // plaintextAddress is the plaintext address to reach the S2A. | ||
| private String plaintextAddress; | ||
|
|
||
| // mtlsAddress is the mTLS address to reach the S2A. | ||
| private String mtlsAddress; | ||
|
|
||
| Builder() { | ||
| plaintextAddress = ""; | ||
| mtlsAddress = ""; | ||
| } | ||
|
|
||
| @CanIgnoreReturnValue | ||
| public Builder setPlaintextAddress(String plaintextAddress) { | ||
| /* | ||
| * No validation / format check is necessary here. It is up to the client which consumes this address | ||
| * to return error if there is a problem connecting to S2A at that address. | ||
| */ | ||
| this.plaintextAddress = plaintextAddress; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does these addresses need any validation or format check when setting with builder?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No validation/format check is necessary here, because we own the MDS endpoint that is being queried to get the address, and it is up to the client which consumes the address (S2A client) to return error if there is a problem connecting to the S2A at that address.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add this explanation as javadoc comment here or to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 4d05638 |
||
| return this; | ||
| } | ||
|
|
||
| @CanIgnoreReturnValue | ||
| public Builder setMtlsAddress(String mtlsAddress) { | ||
| /* | ||
| * No validation / format check is necessary here. It is up to the client which consumes this address | ||
| * to return error if there is a problem connecting to S2A at that address. | ||
| */ | ||
| this.mtlsAddress = mtlsAddress; | ||
| return this; | ||
| } | ||
|
|
||
| public S2AConfig build() { | ||
| return new S2AConfig(plaintextAddress, mtlsAddress); | ||
| } | ||
| } | ||
|
|
||
| private S2AConfig(String plaintextAddress, String mtlsAddress) { | ||
| this.plaintextAddress = plaintextAddress; | ||
| this.mtlsAddress = mtlsAddress; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.