Skip to content

Commit 35d789f

Browse files
authored
feat: Validate the Universe Domain inside Java-Core (#2592)
Validate the Universe Domain prior to the request being initialized and executed. The request will throw an `IllegalStateException` if the validation fails (configured Universe Domain does not match the Credentials' Universe Domain).
1 parent ad641e5 commit 35d789f

File tree

3 files changed

+308
-2
lines changed

3 files changed

+308
-2
lines changed

gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
@InternalApi
4848
@AutoValue
4949
public abstract class EndpointContext {
50-
private static final String GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN";
51-
private static final String INVALID_UNIVERSE_DOMAIN_ERROR_TEMPLATE =
50+
public static final String GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN";
51+
public static final String INVALID_UNIVERSE_DOMAIN_ERROR_TEMPLATE =
5252
"The configured universe domain (%s) does not match the universe domain found in the credentials (%s). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.";
5353
public static final String UNABLE_TO_RETRIEVE_CREDENTIALS_ERROR_MESSAGE =
5454
"Unable to retrieve the Universe Domain from the Credentials.";

java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java

+41
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@
2525
import com.google.api.client.http.javanet.NetHttpTransport;
2626
import com.google.api.gax.core.GaxProperties;
2727
import com.google.api.gax.httpjson.HttpHeadersUtils;
28+
import com.google.api.gax.httpjson.HttpJsonStatusCode;
2829
import com.google.api.gax.rpc.ApiClientHeaderProvider;
30+
import com.google.api.gax.rpc.EndpointContext;
2931
import com.google.api.gax.rpc.HeaderProvider;
32+
import com.google.api.gax.rpc.StatusCode;
33+
import com.google.api.gax.rpc.UnauthenticatedException;
3034
import com.google.auth.Credentials;
3135
import com.google.auth.http.HttpCredentialsAdapter;
3236
import com.google.auth.http.HttpTransportFactory;
@@ -153,11 +157,48 @@ public HttpRequestInitializer getHttpRequestInitializer(
153157
serviceOptions.getMergedHeaderProvider(internalHeaderProvider);
154158

155159
return new HttpRequestInitializer() {
160+
161+
/**
162+
* Helper method to resolve the Universe Domain. First checks the user configuration from
163+
* ServiceOptions, then the Environment Variable. If both haven't been set, resolve the value
164+
* to be the Google Default Universe.
165+
*/
166+
private String determineUniverseDomain() {
167+
String universeDomain = serviceOptions.getUniverseDomain();
168+
if (universeDomain == null) {
169+
universeDomain = System.getenv(EndpointContext.GOOGLE_CLOUD_UNIVERSE_DOMAIN);
170+
}
171+
return universeDomain == null ? Credentials.GOOGLE_DEFAULT_UNIVERSE : universeDomain;
172+
}
173+
156174
@Override
157175
public void initialize(HttpRequest httpRequest) throws IOException {
176+
String configuredUniverseDomain = determineUniverseDomain();
177+
// Default to the GDU. Override with value in the Credentials if needed
178+
String credentialsUniverseDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE;
179+
180+
// delegate is always HttpCredentialsAdapter or null (NoCredentials)
181+
if (delegate != null) {
182+
HttpCredentialsAdapter httpCredentialsAdapter = (HttpCredentialsAdapter) delegate;
183+
credentialsUniverseDomain = httpCredentialsAdapter.getCredentials().getUniverseDomain();
184+
}
185+
186+
// Validate the universe domain before initializing the request
187+
if (!configuredUniverseDomain.equals(credentialsUniverseDomain)) {
188+
throw new UnauthenticatedException(
189+
new Throwable(
190+
String.format(
191+
EndpointContext.INVALID_UNIVERSE_DOMAIN_ERROR_TEMPLATE,
192+
configuredUniverseDomain,
193+
credentialsUniverseDomain)),
194+
HttpJsonStatusCode.of(StatusCode.Code.UNAUTHENTICATED),
195+
false);
196+
}
197+
158198
if (delegate != null) {
159199
delegate.initialize(httpRequest);
160200
}
201+
161202
if (connectTimeout >= 0) {
162203
httpRequest.setConnectTimeout(connectTimeout);
163204
}

java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java

+265
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,53 @@
1919
import static org.junit.Assert.assertEquals;
2020
import static org.junit.Assert.assertNotEquals;
2121
import static org.junit.Assert.assertSame;
22+
import static org.junit.Assert.assertThrows;
2223
import static org.junit.Assert.assertTrue;
2324

25+
import com.google.api.client.http.HttpRequest;
26+
import com.google.api.client.http.HttpRequestInitializer;
27+
import com.google.api.client.http.HttpTransport;
28+
import com.google.api.client.http.LowLevelHttpRequest;
29+
import com.google.api.client.http.LowLevelHttpResponse;
30+
import com.google.api.client.testing.http.HttpTesting;
31+
import com.google.api.client.testing.http.MockHttpTransport;
32+
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
33+
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
2434
import com.google.api.gax.rpc.HeaderProvider;
35+
import com.google.api.gax.rpc.UnauthenticatedException;
36+
import com.google.auth.Credentials;
2537
import com.google.auth.http.HttpTransportFactory;
38+
import com.google.cloud.BaseService;
39+
import com.google.cloud.NoCredentials;
40+
import com.google.cloud.Service;
41+
import com.google.cloud.ServiceDefaults;
42+
import com.google.cloud.ServiceFactory;
2643
import com.google.cloud.ServiceOptions;
44+
import com.google.cloud.ServiceRpc;
45+
import com.google.cloud.TransportOptions;
2746
import com.google.cloud.http.HttpTransportOptions.DefaultHttpTransportFactory;
47+
import com.google.cloud.spi.ServiceRpcFactory;
48+
import java.io.IOException;
49+
import java.util.HashMap;
50+
import java.util.Set;
2851
import java.util.regex.Pattern;
2952
import org.easymock.EasyMock;
53+
import org.junit.Before;
3054
import org.junit.Test;
3155

3256
public class HttpTransportOptionsTest {
57+
private static final HttpTransport MOCK_HTTP_TRANSPORT =
58+
new MockHttpTransport() {
59+
@Override
60+
public LowLevelHttpRequest buildRequest(String method, String url) {
61+
return new MockLowLevelHttpRequest() {
62+
@Override
63+
public LowLevelHttpResponse execute() {
64+
return new MockLowLevelHttpResponse();
65+
}
66+
};
67+
}
68+
};
3369

3470
private static final HttpTransportFactory MOCK_HTTP_TRANSPORT_FACTORY =
3571
EasyMock.createMock(HttpTransportFactory.class);
@@ -42,6 +78,35 @@ public class HttpTransportOptionsTest {
4278
private static final HttpTransportOptions DEFAULT_OPTIONS =
4379
HttpTransportOptions.newBuilder().build();
4480
private static final HttpTransportOptions OPTIONS_COPY = OPTIONS.toBuilder().build();
81+
private static final String DEFAULT_PROJECT_ID = "testing";
82+
private static final String CUSTOM_UNIVERSE_DOMAIN = "random.com";
83+
84+
private HeaderProvider defaultHeaderProvider;
85+
// Credentials' getUniverseDomain() returns GDU
86+
private Credentials defaultCredentials;
87+
// Credentials' getUniverseDomain() returns `random.com`
88+
private Credentials customCredentials;
89+
private HttpRequest defaultHttpRequest;
90+
91+
@Before
92+
public void setup() throws IOException {
93+
defaultHeaderProvider = EasyMock.createMock(HeaderProvider.class);
94+
EasyMock.expect(defaultHeaderProvider.getHeaders()).andReturn(new HashMap<>());
95+
96+
defaultCredentials = EasyMock.createMock(Credentials.class);
97+
EasyMock.expect(defaultCredentials.getUniverseDomain())
98+
.andReturn(Credentials.GOOGLE_DEFAULT_UNIVERSE);
99+
EasyMock.expect(defaultCredentials.hasRequestMetadata()).andReturn(false);
100+
101+
customCredentials = EasyMock.createMock(Credentials.class);
102+
EasyMock.expect(customCredentials.getUniverseDomain()).andReturn(CUSTOM_UNIVERSE_DOMAIN);
103+
EasyMock.expect(customCredentials.hasRequestMetadata()).andReturn(false);
104+
105+
EasyMock.replay(defaultHeaderProvider, defaultCredentials, customCredentials);
106+
107+
defaultHttpRequest =
108+
MOCK_HTTP_TRANSPORT.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
109+
}
45110

46111
@Test
47112
public void testBuilder() {
@@ -78,4 +143,204 @@ public void testHeader() {
78143
.matcher(headerProvider.getHeaders().values().iterator().next())
79144
.find());
80145
}
146+
147+
@Test
148+
public void testHttpRequestInitializer_defaultUniverseDomainSettings_defaultCredentials()
149+
throws IOException {
150+
TestServiceOptions testServiceOptions =
151+
generateTestServiceOptions(Credentials.GOOGLE_DEFAULT_UNIVERSE, defaultCredentials);
152+
HttpRequestInitializer httpRequestInitializer =
153+
DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions);
154+
// Does not throw a validation exception
155+
httpRequestInitializer.initialize(defaultHttpRequest);
156+
}
157+
158+
@Test
159+
public void testHttpRequestInitializer_defaultUniverseDomainSettings_customCredentials() {
160+
TestServiceOptions testServiceOptions =
161+
generateTestServiceOptions(Credentials.GOOGLE_DEFAULT_UNIVERSE, customCredentials);
162+
HttpRequestInitializer httpRequestInitializer =
163+
DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions);
164+
UnauthenticatedException exception =
165+
assertThrows(
166+
UnauthenticatedException.class,
167+
() -> httpRequestInitializer.initialize(defaultHttpRequest));
168+
assertEquals(
169+
"The configured universe domain (googleapis.com) does not match the universe domain found in the credentials (random.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.",
170+
exception.getCause().getMessage());
171+
}
172+
173+
@Test
174+
public void testHttpRequestInitializer_customUniverseDomainSettings_defaultCredentials() {
175+
TestServiceOptions testServiceOptions =
176+
generateTestServiceOptions(CUSTOM_UNIVERSE_DOMAIN, defaultCredentials);
177+
HttpRequestInitializer httpRequestInitializer =
178+
DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions);
179+
UnauthenticatedException exception =
180+
assertThrows(
181+
UnauthenticatedException.class,
182+
() -> httpRequestInitializer.initialize(defaultHttpRequest));
183+
assertEquals(
184+
"The configured universe domain (random.com) does not match the universe domain found in the credentials (googleapis.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.",
185+
exception.getCause().getMessage());
186+
}
187+
188+
@Test
189+
public void testHttpRequestInitializer_customUniverseDomainSettings_customCredentials()
190+
throws IOException {
191+
TestServiceOptions testServiceOptions =
192+
generateTestServiceOptions(CUSTOM_UNIVERSE_DOMAIN, customCredentials);
193+
HttpRequestInitializer httpRequestInitializer =
194+
DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions);
195+
// Does not throw a validation exception
196+
httpRequestInitializer.initialize(defaultHttpRequest);
197+
}
198+
199+
@Test
200+
public void testHttpRequestInitializer_defaultUniverseDomainSettings_noCredentials()
201+
throws IOException {
202+
NoCredentials noCredentials = NoCredentials.getInstance();
203+
TestServiceOptions testServiceOptions =
204+
generateTestServiceOptions(Credentials.GOOGLE_DEFAULT_UNIVERSE, noCredentials);
205+
HttpRequestInitializer httpRequestInitializer =
206+
DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions);
207+
// Does not throw a validation exception
208+
httpRequestInitializer.initialize(defaultHttpRequest);
209+
}
210+
211+
@Test
212+
public void testHttpRequestInitializer_customUniverseDomainSettings_noCredentials() {
213+
NoCredentials noCredentials = NoCredentials.getInstance();
214+
TestServiceOptions testServiceOptions =
215+
generateTestServiceOptions(CUSTOM_UNIVERSE_DOMAIN, noCredentials);
216+
HttpRequestInitializer httpRequestInitializer =
217+
DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions);
218+
UnauthenticatedException exception =
219+
assertThrows(
220+
UnauthenticatedException.class,
221+
() -> httpRequestInitializer.initialize(defaultHttpRequest));
222+
assertEquals(
223+
"The configured universe domain (random.com) does not match the universe domain found in the credentials (googleapis.com). If you haven't configured the universe domain explicitly, `googleapis.com` is the default.",
224+
exception.getCause().getMessage());
225+
}
226+
227+
private TestServiceOptions generateTestServiceOptions(
228+
String universeDomain, Credentials credentials) {
229+
return TestServiceOptions.newBuilder()
230+
.setCredentials(credentials)
231+
.setHeaderProvider(defaultHeaderProvider)
232+
.setQuotaProjectId(DEFAULT_PROJECT_ID)
233+
.setProjectId(DEFAULT_PROJECT_ID)
234+
.setUniverseDomain(universeDomain)
235+
.build();
236+
}
237+
238+
/**
239+
* The following interfaces and classes are from ServiceOptionsTest. Copied over here as
240+
* ServiceOptions resides inside google-cloud-core test folder and is not accessible from
241+
* google-cloud-core-http.
242+
*/
243+
interface TestService extends Service<TestServiceOptions> {}
244+
245+
private static class TestServiceImpl extends BaseService<TestServiceOptions>
246+
implements TestService {
247+
private TestServiceImpl(TestServiceOptions options) {
248+
super(options);
249+
}
250+
}
251+
252+
public interface TestServiceFactory extends ServiceFactory<TestService, TestServiceOptions> {}
253+
254+
private static class DefaultTestServiceFactory implements TestServiceFactory {
255+
private static final TestServiceFactory INSTANCE = new DefaultTestServiceFactory();
256+
257+
@Override
258+
public TestService create(TestServiceOptions options) {
259+
return new TestServiceImpl(options);
260+
}
261+
}
262+
263+
public interface TestServiceRpcFactory extends ServiceRpcFactory<TestServiceOptions> {}
264+
265+
private static class DefaultTestServiceRpcFactory implements TestServiceRpcFactory {
266+
private static final TestServiceRpcFactory INSTANCE = new DefaultTestServiceRpcFactory();
267+
268+
@Override
269+
public TestServiceRpc create(TestServiceOptions options) {
270+
return new DefaultTestServiceRpc(options);
271+
}
272+
}
273+
274+
private interface TestServiceRpc extends ServiceRpc {}
275+
276+
private static class DefaultTestServiceRpc implements TestServiceRpc {
277+
DefaultTestServiceRpc(TestServiceOptions options) {}
278+
}
279+
280+
static class TestServiceOptions extends ServiceOptions<TestService, TestServiceOptions> {
281+
private static class Builder
282+
extends ServiceOptions.Builder<TestService, TestServiceOptions, Builder> {
283+
private Builder() {}
284+
285+
private Builder(TestServiceOptions options) {
286+
super(options);
287+
}
288+
289+
@Override
290+
protected TestServiceOptions build() {
291+
return new TestServiceOptions(this);
292+
}
293+
}
294+
295+
private TestServiceOptions(Builder builder) {
296+
super(
297+
TestServiceFactory.class,
298+
TestServiceRpcFactory.class,
299+
builder,
300+
new TestServiceDefaults());
301+
}
302+
303+
private static class TestServiceDefaults
304+
implements ServiceDefaults<TestService, TestServiceOptions> {
305+
306+
@Override
307+
public TestServiceFactory getDefaultServiceFactory() {
308+
return DefaultTestServiceFactory.INSTANCE;
309+
}
310+
311+
@Override
312+
public TestServiceRpcFactory getDefaultRpcFactory() {
313+
return DefaultTestServiceRpcFactory.INSTANCE;
314+
}
315+
316+
@Override
317+
public TransportOptions getDefaultTransportOptions() {
318+
return new TransportOptions() {};
319+
}
320+
}
321+
322+
@Override
323+
protected Set<String> getScopes() {
324+
return null;
325+
}
326+
327+
@Override
328+
public Builder toBuilder() {
329+
return new Builder(this);
330+
}
331+
332+
private static Builder newBuilder() {
333+
return new Builder();
334+
}
335+
336+
@Override
337+
public boolean equals(Object obj) {
338+
return obj instanceof TestServiceOptions && baseEquals((TestServiceOptions) obj);
339+
}
340+
341+
@Override
342+
public int hashCode() {
343+
return baseHashCode();
344+
}
345+
}
81346
}

0 commit comments

Comments
 (0)