Skip to content

Commit 38bdf60

Browse files
aeitzmanlsirac
andauthored
feat: Byoid metrics framework (#1232)
* feat: adding byoid metrics framework * fix: formatting * fix: formatting * Addressing PR comments * Update oauth2_http/java/com/google/auth/oauth2/MetricsUtils.java Co-authored-by: Leo <[email protected]> * Update oauth2_http/java/com/google/auth/oauth2/MetricsUtils.java Co-authored-by: Leo <[email protected]> * Update oauth2_http/java/com/google/auth/oauth2/MetricsUtils.java Co-authored-by: Leo <[email protected]> * fix test * formatting * regex fix * lint fix * remove builder method and fix tests * rename handler class, fix tests * addressing comments, credential source type --------- Co-authored-by: Leo <[email protected]>
1 parent d11ca69 commit 38bdf60

14 files changed

+400
-17
lines changed

oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java

+5
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ public GoogleCredentials createScoped(Collection<String> newScopes) {
197197
return new AwsCredentials((AwsCredentials.Builder) newBuilder(this).setScopes(newScopes));
198198
}
199199

200+
@Override
201+
String getCredentialSourceType() {
202+
return "aws";
203+
}
204+
200205
private String retrieveResource(String url, String resourceName, Map<String, Object> headers)
201206
throws IOException {
202207
return retrieveResource(url, resourceName, HttpMethods.GET, headers, /* content= */ null);

oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import static com.google.common.base.Preconditions.checkNotNull;
3535

36+
import com.google.api.client.http.HttpHeaders;
3637
import com.google.api.client.json.GenericJson;
3738
import com.google.api.client.json.JsonObjectParser;
3839
import com.google.auth.RequestMetadataCallback;
@@ -90,6 +91,7 @@ abstract static class CredentialSource implements java.io.Serializable {
9091
private final CredentialSource credentialSource;
9192
private final Collection<String> scopes;
9293
private final ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;
94+
private ExternalAccountMetricsHandler metricsHandler;
9395

9496
@Nullable private final String tokenInfoUrl;
9597
@Nullable private final String serviceAccountImpersonationUrl;
@@ -224,6 +226,8 @@ protected ExternalAccountCredentials(
224226
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
225227
}
226228

229+
this.metricsHandler = new ExternalAccountMetricsHandler(this);
230+
227231
this.impersonatedCredentials = buildImpersonatedCredentials();
228232
}
229233

@@ -274,6 +278,11 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
274278
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
275279
}
276280

281+
this.metricsHandler =
282+
builder.metricsHandler == null
283+
? new ExternalAccountMetricsHandler(this)
284+
: builder.metricsHandler;
285+
277286
this.impersonatedCredentials = buildImpersonatedCredentials();
278287
}
279288

@@ -505,6 +514,12 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
505514
requestHandler.setInternalOptions(options.toString());
506515
}
507516

517+
// Set BYOID Metrics header.
518+
HttpHeaders additionalHeaders = new HttpHeaders();
519+
additionalHeaders.set(
520+
MetricsUtils.API_CLIENT_HEADER, this.metricsHandler.getExternalAccountMetricsHeader());
521+
requestHandler.setHeaders(additionalHeaders);
522+
508523
if (stsTokenExchangeRequest.getInternalOptions() != null) {
509524
// Overwrite internal options. Let subclass handle setting options.
510525
requestHandler.setInternalOptions(stsTokenExchangeRequest.getInternalOptions());
@@ -589,6 +604,10 @@ public ServiceAccountImpersonationOptions getServiceAccountImpersonationOptions(
589604
return serviceAccountImpersonationOptions;
590605
}
591606

607+
String getCredentialSourceType() {
608+
return "unknown";
609+
}
610+
592611
EnvironmentProvider getEnvironmentProvider() {
593612
return environmentProvider;
594613
}
@@ -663,8 +682,11 @@ static final class ServiceAccountImpersonationOptions implements java.io.Seriali
663682

664683
private final int lifetime;
665684

685+
final boolean customTokenLifetimeRequested;
686+
666687
ServiceAccountImpersonationOptions(Map<String, Object> optionsMap) {
667-
if (!optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) {
688+
customTokenLifetimeRequested = optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY);
689+
if (!customTokenLifetimeRequested) {
668690
lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS;
669691
return;
670692
}
@@ -714,6 +736,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
714736
@Nullable protected String workforcePoolUserProject;
715737
@Nullable protected ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;
716738
@Nullable protected String universeDomain;
739+
@Nullable protected ExternalAccountMetricsHandler metricsHandler;
717740

718741
protected Builder() {}
719742

@@ -733,6 +756,7 @@ protected Builder(ExternalAccountCredentials credentials) {
733756
this.workforcePoolUserProject = credentials.workforcePoolUserProject;
734757
this.serviceAccountImpersonationOptions = credentials.serviceAccountImpersonationOptions;
735758
this.universeDomain = credentials.universeDomain;
759+
this.metricsHandler = credentials.metricsHandler;
736760
}
737761

738762
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.oauth2;
33+
34+
/**
35+
* A handler for generating the x-goog-api-client header value for BYOID external account
36+
* credentials.
37+
*/
38+
class ExternalAccountMetricsHandler implements java.io.Serializable {
39+
private static final String SOURCE_KEY = "source";
40+
private static final String IMPERSONATION_KEY = "sa-impersonation";
41+
private static final String CONFIG_LIFETIME_KEY = "config-lifetime";
42+
private static final String BYOID_METRICS_SECTION = "google-byoid-sdk";
43+
44+
private final boolean configLifetime;
45+
private final boolean saImpersonation;
46+
private String credentialSourceType;
47+
48+
/**
49+
* Constructor for the external account metrics handler.
50+
*
51+
* @param creds the {@code ExternalAccountCredentials} object to set the external account metrics
52+
* options from.
53+
*/
54+
ExternalAccountMetricsHandler(ExternalAccountCredentials creds) {
55+
this.saImpersonation = creds.getServiceAccountImpersonationUrl() != null;
56+
this.configLifetime =
57+
creds.getServiceAccountImpersonationOptions().customTokenLifetimeRequested;
58+
this.credentialSourceType = creds.getCredentialSourceType();
59+
}
60+
61+
/**
62+
* Gets the external account metrics header value for the x-goog-api-client header.
63+
*
64+
* @return the header value.
65+
*/
66+
String getExternalAccountMetricsHeader() {
67+
return String.format(
68+
"%s %s %s/%s %s/%s %s/%s",
69+
MetricsUtils.getLanguageAndAuthLibraryVersions(),
70+
BYOID_METRICS_SECTION,
71+
SOURCE_KEY,
72+
this.credentialSourceType,
73+
IMPERSONATION_KEY,
74+
this.saImpersonation,
75+
CONFIG_LIFETIME_KEY,
76+
this.configLifetime);
77+
}
78+
}

oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java

+11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.google.api.client.json.GenericJson;
3939
import com.google.api.client.json.JsonObjectParser;
4040
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType;
41+
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.IdentityPoolCredentialSourceType;
4142
import com.google.common.io.CharStreams;
4243
import java.io.BufferedReader;
4344
import java.io.File;
@@ -192,6 +193,16 @@ public String retrieveSubjectToken() throws IOException {
192193
return getSubjectTokenFromMetadataServer();
193194
}
194195

196+
@Override
197+
String getCredentialSourceType() {
198+
if (((IdentityPoolCredentialSource) this.getCredentialSource()).credentialSourceType
199+
== IdentityPoolCredentialSourceType.FILE) {
200+
return "file";
201+
} else {
202+
return "url";
203+
}
204+
}
205+
195206
private String retrieveSubjectTokenFromCredentialFile() throws IOException {
196207
String credentialFilePath = identityPoolCredentialSource.credentialLocation;
197208
if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.oauth2;
33+
34+
import java.io.IOException;
35+
import java.io.InputStream;
36+
import java.util.Properties;
37+
38+
class MetricsUtils {
39+
static final String API_CLIENT_HEADER = "x-goog-api-client";
40+
private static final String authLibraryVersion = getAuthLibraryVersion();
41+
private static final String javaLanguageVersion = System.getProperty("java.version");
42+
43+
/**
44+
* Gets the x-goog-api-client header value for the current Java language version and the auth
45+
* library version.
46+
*
47+
* @return the header value.
48+
*/
49+
static String getLanguageAndAuthLibraryVersions() {
50+
return String.format("gl-java/%s auth/%s", javaLanguageVersion, authLibraryVersion);
51+
}
52+
53+
private static String getAuthLibraryVersion() {
54+
// Attempt to read the library's version from a properties file generated during the build.
55+
// This value should be read and cached for later use.
56+
String version = "unknown-version";
57+
try (InputStream inputStream =
58+
MetricsUtils.class.getResourceAsStream(
59+
"/com/google/auth/oauth2/google-auth-library.properties")) {
60+
if (inputStream != null) {
61+
final Properties properties = new Properties();
62+
properties.load(inputStream);
63+
version = properties.getProperty("google-auth-library.version");
64+
}
65+
} catch (IOException e) {
66+
// Ignore.
67+
}
68+
return version;
69+
}
70+
}

oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java

+5
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,11 @@ public PluggableAuthCredentials createScoped(Collection<String> newScopes) {
292292
(PluggableAuthCredentials.Builder) newBuilder(this).setScopes(newScopes));
293293
}
294294

295+
@Override
296+
String getCredentialSourceType() {
297+
return "executable";
298+
}
299+
295300
public static Builder newBuilder() {
296301
return new Builder();
297302
}

oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java

+27-6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOExc
131131
AccessToken accessToken = awsCredential.refreshAccessToken();
132132

133133
assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
134+
135+
// Validate metrics header is set correctly on the sts request.
136+
Map<String, List<String>> headers =
137+
transportFactory.transport.getRequests().get(3).getHeaders();
138+
ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", false, false);
134139
}
135140

136141
@Test
@@ -142,18 +147,26 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept
142147

143148
AwsCredentials awsCredential =
144149
(AwsCredentials)
145-
AwsCredentials.newBuilder(AWS_CREDENTIAL)
150+
AwsCredentials.newBuilder()
151+
.setHttpTransportFactory(transportFactory)
152+
.setAudience("audience")
153+
.setSubjectTokenType("subjectTokenType")
146154
.setTokenUrl(transportFactory.transport.getStsUrl())
155+
.setTokenInfoUrl("tokenInfoUrl")
156+
.setCredentialSource(buildAwsCredentialSource(transportFactory))
147157
.setServiceAccountImpersonationUrl(
148158
transportFactory.transport.getServiceAccountImpersonationUrl())
149-
.setHttpTransportFactory(transportFactory)
150-
.setCredentialSource(buildAwsCredentialSource(transportFactory))
151159
.build();
152160

153161
AccessToken accessToken = awsCredential.refreshAccessToken();
154162

155163
assertEquals(
156164
transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());
165+
166+
// Validate metrics header is set correctly on the sts request.
167+
Map<String, List<String>> headers =
168+
transportFactory.transport.getRequests().get(6).getHeaders();
169+
ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", true, false);
157170
}
158171

159172
@Test
@@ -165,12 +178,15 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I
165178

166179
AwsCredentials awsCredential =
167180
(AwsCredentials)
168-
AwsCredentials.newBuilder(AWS_CREDENTIAL)
181+
AwsCredentials.newBuilder()
182+
.setHttpTransportFactory(transportFactory)
183+
.setAudience("audience")
184+
.setSubjectTokenType("subjectTokenType")
169185
.setTokenUrl(transportFactory.transport.getStsUrl())
186+
.setTokenInfoUrl("tokenInfoUrl")
187+
.setCredentialSource(buildAwsCredentialSource(transportFactory))
170188
.setServiceAccountImpersonationUrl(
171189
transportFactory.transport.getServiceAccountImpersonationUrl())
172-
.setHttpTransportFactory(transportFactory)
173-
.setCredentialSource(buildAwsCredentialSource(transportFactory))
174190
.setServiceAccountImpersonationOptions(
175191
ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800))
176192
.build();
@@ -187,6 +203,11 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I
187203
.parseAndClose(GenericJson.class);
188204

189205
assertEquals("2800s", query.get("lifetime"));
206+
207+
// Validate metrics header is set correctly on the sts request.
208+
Map<String, List<String>> headers =
209+
transportFactory.transport.getRequests().get(6).getHeaders();
210+
ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", true, true);
190211
}
191212

192213
@Test

0 commit comments

Comments
 (0)