Skip to content

Commit 4adfed9

Browse files
authored
feat: Add Universe Domain Support (#2435)
* feat: Implement Universe Domain Support * chore: Clean up auth dependencies * chore: Resolve checkstyle issues * chore: Determine service name from url * chore: Return null for invalid rootUrl * chore: Add javadocs for Universe Domain changes in AbstractGoogleClient * chore: Address checkstyle issues * chore: Add tests for AbstractGoogleClient * chore: Fix tests * chore: Update docs for AbstractGoogleClient * chore: validateUniverseDomain does not return bool * chore: Move validateUniverseDomain() to parent impl * chore: Update javadocs * chore: Add Env Var tests for GOOGLE_CLOUD_UNIVERSE_DOMAIN * chore: Fix lint issues * chore: Fix env var tests * chore: Update logic for isUserConfiguredEndpoint * chore: Throw IOException on validateUniverseDomain * chore: Address PR comments * chore: Address PR comments * chore: Address PR comments * chore: Address PR comments * chore: Validate on HttpCredentialsAdapter * chore: Address PR comments
1 parent 42b6c9e commit 4adfed9

File tree

5 files changed

+526
-6
lines changed

5 files changed

+526
-6
lines changed

.github/workflows/ci.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ jobs:
2020
- run: .kokoro/build.sh
2121
env:
2222
JOB_TYPE: test
23+
# The `envVarTest` profile runs tests that require an environment variable
24+
- name: Env Var Tests
25+
run: |
26+
mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true -PenvVarTest
27+
# Set the Env Var for this step only
28+
env:
29+
GOOGLE_CLOUD_UNIVERSE_DOMAIN: random.com
2330
windows:
2431
runs-on: windows-latest
2532
steps:

google-api-client/pom.xml

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@
111111
<usedDependencies>commons-codec:commons-codec</usedDependencies>
112112
</configuration>
113113
</plugin>
114+
<plugin>
115+
<groupId>org.apache.maven.plugins</groupId>
116+
<artifactId>maven-surefire-plugin</artifactId>
117+
<configuration>
118+
<!-- These tests require an Env Var to be set. Use -PenvVarTest to ONLY run these tests -->
119+
<test>!AbstractGoogleClientTest#testGoogleClientBuilder_noCustomUniverseDomain_universeDomainEnvVar+testGoogleClientBuilder_customUniverseDomain_universeDomainEnvVar</test>
120+
</configuration>
121+
</plugin>
114122
</plugins>
115123

116124
<resources>
@@ -135,6 +143,14 @@
135143
<groupId>com.google.oauth-client</groupId>
136144
<artifactId>google-oauth-client</artifactId>
137145
</dependency>
146+
<dependency>
147+
<groupId>com.google.auth</groupId>
148+
<artifactId>google-auth-library-credentials</artifactId>
149+
</dependency>
150+
<dependency>
151+
<groupId>com.google.auth</groupId>
152+
<artifactId>google-auth-library-oauth2-http</artifactId>
153+
</dependency>
138154
<dependency>
139155
<groupId>com.google.http-client</groupId>
140156
<artifactId>google-http-client-gson</artifactId>
@@ -179,6 +195,27 @@
179195
<artifactId>junit</artifactId>
180196
<scope>test</scope>
181197
</dependency>
182-
198+
<dependency>
199+
<groupId>org.mockito</groupId>
200+
<artifactId>mockito-core</artifactId>
201+
<scope>test</scope>
202+
</dependency>
183203
</dependencies>
204+
205+
<profiles>
206+
<profile>
207+
<id>envVarTest</id>
208+
<build>
209+
<plugins>
210+
<plugin>
211+
<groupId>org.apache.maven.plugins</groupId>
212+
<artifactId>maven-surefire-plugin</artifactId>
213+
<configuration>
214+
<test>AbstractGoogleClientTest#testGoogleClientBuilder_noCustomUniverseDomain_universeDomainEnvVar+testGoogleClientBuilder_customUniverseDomain_universeDomainEnvVar</test>
215+
</configuration>
216+
</plugin>
217+
</plugins>
218+
</build>
219+
</profile>
220+
</profiles>
184221
</project>

google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClient.java

Lines changed: 150 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@
2020
import com.google.api.client.util.ObjectParser;
2121
import com.google.api.client.util.Preconditions;
2222
import com.google.api.client.util.Strings;
23+
import com.google.auth.Credentials;
24+
import com.google.auth.http.HttpCredentialsAdapter;
25+
import com.google.common.annotations.VisibleForTesting;
2326
import java.io.IOException;
2427
import java.util.logging.Logger;
28+
import java.util.regex.Matcher;
29+
import java.util.regex.Pattern;
2530

2631
/**
2732
* Abstract thread-safe Google client.
@@ -33,6 +38,8 @@ public abstract class AbstractGoogleClient {
3338

3439
private static final Logger logger = Logger.getLogger(AbstractGoogleClient.class.getName());
3540

41+
private static final String GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN";
42+
3643
/** The request factory for connections to the server. */
3744
private final HttpRequestFactory requestFactory;
3845

@@ -68,13 +75,18 @@ public abstract class AbstractGoogleClient {
6875
/** Whether discovery required parameter checks should be suppressed. */
6976
private final boolean suppressRequiredParameterChecks;
7077

78+
private final String universeDomain;
79+
80+
private final HttpRequestInitializer httpRequestInitializer;
81+
7182
/**
7283
* @param builder builder
7384
* @since 1.14
7485
*/
7586
protected AbstractGoogleClient(Builder builder) {
7687
googleClientRequestInitializer = builder.googleClientRequestInitializer;
77-
rootUrl = normalizeRootUrl(builder.rootUrl);
88+
universeDomain = determineUniverseDomain(builder);
89+
rootUrl = normalizeRootUrl(determineEndpoint(builder));
7890
servicePath = normalizeServicePath(builder.servicePath);
7991
batchPath = builder.batchPath;
8092
if (Strings.isNullOrEmpty(builder.applicationName)) {
@@ -88,6 +100,75 @@ protected AbstractGoogleClient(Builder builder) {
88100
objectParser = builder.objectParser;
89101
suppressPatternChecks = builder.suppressPatternChecks;
90102
suppressRequiredParameterChecks = builder.suppressRequiredParameterChecks;
103+
httpRequestInitializer = builder.httpRequestInitializer;
104+
}
105+
106+
/**
107+
* Resolve the Universe Domain to be used when resolving the endpoint. The logic for resolving the
108+
* universe domain is the following order: 1. Use the user configured value is set, 2. Use the
109+
* Universe Domain Env Var if set, 3. Default to the Google Default Universe
110+
*/
111+
private String determineUniverseDomain(Builder builder) {
112+
String resolvedUniverseDomain = builder.universeDomain;
113+
if (resolvedUniverseDomain == null) {
114+
resolvedUniverseDomain = System.getenv(GOOGLE_CLOUD_UNIVERSE_DOMAIN);
115+
}
116+
return resolvedUniverseDomain == null
117+
? Credentials.GOOGLE_DEFAULT_UNIVERSE
118+
: resolvedUniverseDomain;
119+
}
120+
121+
/**
122+
* Resolve the endpoint based on user configurations. If the user has configured a custom rootUrl,
123+
* use that value. Otherwise, construct the endpoint based on the serviceName and the
124+
* universeDomain.
125+
*/
126+
private String determineEndpoint(Builder builder) {
127+
boolean mtlsEnabled = builder.rootUrl.contains(".mtls.");
128+
if (mtlsEnabled && !universeDomain.equals(Credentials.GOOGLE_DEFAULT_UNIVERSE)) {
129+
throw new IllegalStateException(
130+
"mTLS is not supported in any universe other than googleapis.com");
131+
}
132+
// If the serviceName is null, we cannot construct a valid resolved endpoint. Simply return
133+
// the rootUrl as this was custom rootUrl passed in.
134+
if (builder.isUserConfiguredEndpoint || builder.serviceName == null) {
135+
return builder.rootUrl;
136+
}
137+
if (mtlsEnabled) {
138+
return "https://" + builder.serviceName + ".mtls." + universeDomain + "/";
139+
}
140+
return "https://" + builder.serviceName + "." + universeDomain + "/";
141+
}
142+
143+
/**
144+
* Check that the User configured universe domain matches the Credentials' universe domain. This
145+
* uses the HttpRequestInitializer to get the Credentials and is enforced that the
146+
* HttpRequestInitializer is of the {@see <a
147+
* href="https://github.com/googleapis/google-auth-library-java/blob/main/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java">HttpCredentialsAdapter</a>}
148+
* from the google-auth-library.
149+
*
150+
* <p>To use a non-GDU Credentials, you must use the HttpCredentialsAdapter class.
151+
*
152+
* @throws IOException if there is an error reading the Universe Domain from the credentials
153+
* @throws IllegalStateException if the configured Universe Domain does not match the Universe
154+
* Domain in the Credentials
155+
*/
156+
public void validateUniverseDomain() throws IOException {
157+
if (!(httpRequestInitializer instanceof HttpCredentialsAdapter)) {
158+
return;
159+
}
160+
Credentials credentials = ((HttpCredentialsAdapter) httpRequestInitializer).getCredentials();
161+
// No need for a null check as HttpCredentialsAdapter cannot be initialized with null
162+
// Credentials
163+
String expectedUniverseDomain = credentials.getUniverseDomain();
164+
if (!expectedUniverseDomain.equals(getUniverseDomain())) {
165+
throw new IllegalStateException(
166+
String.format(
167+
"The configured universe domain (%s) does not match the universe domain found"
168+
+ " in the credentials (%s). If you haven't configured the universe domain"
169+
+ " explicitly, `googleapis.com` is the default.",
170+
getUniverseDomain(), expectedUniverseDomain));
171+
}
91172
}
92173

93174
/**
@@ -139,6 +220,18 @@ public final GoogleClientRequestInitializer getGoogleClientRequestInitializer()
139220
return googleClientRequestInitializer;
140221
}
141222

223+
/**
224+
* Universe Domain is the domain for Google Cloud Services. It follows the format of
225+
* `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a Universe
226+
* Domain value of `googleapis.com` and cloudasset.test.com would have a Universe Domain of
227+
* `test.com`. If this value is not set, this will default to `googleapis.com`.
228+
*
229+
* @return The configured Universe Domain or the Google Default Universe (googleapis.com)
230+
*/
231+
public final String getUniverseDomain() {
232+
return universeDomain;
233+
}
234+
142235
/**
143236
* Returns the object parser or {@code null} for none.
144237
*
@@ -173,6 +266,7 @@ public ObjectParser getObjectParser() {
173266
* @param httpClientRequest Google client request type
174267
*/
175268
protected void initialize(AbstractGoogleClientRequest<?> httpClientRequest) throws IOException {
269+
validateUniverseDomain();
176270
if (getGoogleClientRequestInitializer() != null) {
177271
getGoogleClientRequestInitializer().initialize(httpClientRequest);
178272
}
@@ -311,6 +405,33 @@ public abstract static class Builder {
311405
/** Whether discovery required parameter checks should be suppressed. */
312406
boolean suppressRequiredParameterChecks;
313407

408+
/** User configured Universe Domain. Defaults to `googleapis.com`. */
409+
String universeDomain;
410+
411+
/**
412+
* Regex pattern to check if the URL passed in matches the default endpoint configured from a
413+
* discovery doc. Follows the format of `https://{serviceName}(.mtls).googleapis.com/`
414+
*/
415+
Pattern defaultEndpointRegex =
416+
Pattern.compile("https://([a-zA-Z]*)(\\.mtls)?\\.googleapis.com/?");
417+
418+
/**
419+
* Whether the user has configured an endpoint via {@link #setRootUrl(String)}. This is added in
420+
* because the rootUrl is set in the Builder's constructor. ,
421+
*
422+
* <p>Apiary clients don't allow user configurations to this Builder's constructor, so this
423+
* would be set to false by default for Apiary libraries. User configuration to the rootUrl is
424+
* done via {@link #setRootUrl(String)}.
425+
*
426+
* <p>For other uses cases that touch this Builder's constructor directly, check if the rootUrl
427+
* passed matches the default endpoint regex. If it doesn't match, it is a user configured
428+
* endpoint.
429+
*/
430+
boolean isUserConfiguredEndpoint;
431+
432+
/** The parsed serviceName value from the rootUrl from the Discovery Doc. */
433+
String serviceName;
434+
314435
/**
315436
* Returns an instance of a new builder.
316437
*
@@ -328,9 +449,15 @@ protected Builder(
328449
HttpRequestInitializer httpRequestInitializer) {
329450
this.transport = Preconditions.checkNotNull(transport);
330451
this.objectParser = objectParser;
331-
setRootUrl(rootUrl);
332-
setServicePath(servicePath);
452+
this.rootUrl = normalizeRootUrl(rootUrl);
453+
this.servicePath = normalizeServicePath(servicePath);
333454
this.httpRequestInitializer = httpRequestInitializer;
455+
Matcher matcher = defaultEndpointRegex.matcher(rootUrl);
456+
boolean matches = matcher.matches();
457+
// Checked here for the use case where users extend this class and may pass in
458+
// a custom endpoint
459+
this.isUserConfiguredEndpoint = !matches;
460+
this.serviceName = matches ? matcher.group(1) : null;
334461
}
335462

336463
/** Builds a new instance of {@link AbstractGoogleClient}. */
@@ -371,6 +498,7 @@ public final String getRootUrl() {
371498
* changing the return type, but nothing else.
372499
*/
373500
public Builder setRootUrl(String rootUrl) {
501+
this.isUserConfiguredEndpoint = true;
374502
this.rootUrl = normalizeRootUrl(rootUrl);
375503
return this;
376504
}
@@ -515,5 +643,24 @@ public Builder setSuppressRequiredParameterChecks(boolean suppressRequiredParame
515643
public Builder setSuppressAllChecks(boolean suppressAllChecks) {
516644
return setSuppressPatternChecks(true).setSuppressRequiredParameterChecks(true);
517645
}
646+
647+
/**
648+
* Sets the user configured Universe Domain value. This value will be used to try and construct
649+
* the endpoint to connect to GCP services.
650+
*
651+
* @throws IllegalArgumentException if universeDomain is passed in with an empty string ("")
652+
*/
653+
public Builder setUniverseDomain(String universeDomain) {
654+
if (universeDomain != null && universeDomain.isEmpty()) {
655+
throw new IllegalArgumentException("The universe domain value cannot be empty.");
656+
}
657+
this.universeDomain = universeDomain;
658+
return this;
659+
}
660+
661+
@VisibleForTesting
662+
String getServiceName() {
663+
return serviceName;
664+
}
518665
}
519666
}

0 commit comments

Comments
 (0)