diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java index c2f3ad50ab..de99b01995 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/EndpointContext.java @@ -47,8 +47,8 @@ @InternalApi @AutoValue public abstract class EndpointContext { - private static final String GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"; - private static final String INVALID_UNIVERSE_DOMAIN_ERROR_TEMPLATE = + public static final String GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"; + public static final String INVALID_UNIVERSE_DOMAIN_ERROR_TEMPLATE = "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."; public static final String UNABLE_TO_RETRIEVE_CREDENTIALS_ERROR_MESSAGE = "Unable to retrieve the Universe Domain from the Credentials."; diff --git a/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java b/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java index 4bdfee3cca..f5ad54532f 100644 --- a/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java +++ b/java-core/google-cloud-core-http/src/main/java/com/google/cloud/http/HttpTransportOptions.java @@ -25,8 +25,12 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.gax.core.GaxProperties; import com.google.api.gax.httpjson.HttpHeadersUtils; +import com.google.api.gax.httpjson.HttpJsonStatusCode; import com.google.api.gax.rpc.ApiClientHeaderProvider; +import com.google.api.gax.rpc.EndpointContext; import com.google.api.gax.rpc.HeaderProvider; +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.rpc.UnauthenticatedException; import com.google.auth.Credentials; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auth.http.HttpTransportFactory; @@ -153,11 +157,48 @@ public HttpRequestInitializer getHttpRequestInitializer( serviceOptions.getMergedHeaderProvider(internalHeaderProvider); return new HttpRequestInitializer() { + + /** + * Helper method to resolve the Universe Domain. First checks the user configuration from + * ServiceOptions, then the Environment Variable. If both haven't been set, resolve the value + * to be the Google Default Universe. + */ + private String determineUniverseDomain() { + String universeDomain = serviceOptions.getUniverseDomain(); + if (universeDomain == null) { + universeDomain = System.getenv(EndpointContext.GOOGLE_CLOUD_UNIVERSE_DOMAIN); + } + return universeDomain == null ? Credentials.GOOGLE_DEFAULT_UNIVERSE : universeDomain; + } + @Override public void initialize(HttpRequest httpRequest) throws IOException { + String configuredUniverseDomain = determineUniverseDomain(); + // Default to the GDU. Override with value in the Credentials if needed + String credentialsUniverseDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE; + + // delegate is always HttpCredentialsAdapter or null (NoCredentials) + if (delegate != null) { + HttpCredentialsAdapter httpCredentialsAdapter = (HttpCredentialsAdapter) delegate; + credentialsUniverseDomain = httpCredentialsAdapter.getCredentials().getUniverseDomain(); + } + + // Validate the universe domain before initializing the request + if (!configuredUniverseDomain.equals(credentialsUniverseDomain)) { + throw new UnauthenticatedException( + new Throwable( + String.format( + EndpointContext.INVALID_UNIVERSE_DOMAIN_ERROR_TEMPLATE, + configuredUniverseDomain, + credentialsUniverseDomain)), + HttpJsonStatusCode.of(StatusCode.Code.UNAUTHENTICATED), + false); + } + if (delegate != null) { delegate.initialize(httpRequest); } + if (connectTimeout >= 0) { httpRequest.setConnectTimeout(connectTimeout); } diff --git a/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java b/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java index 1ff7871aec..75ffc2bf17 100644 --- a/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java +++ b/java-core/google-cloud-core-http/src/test/java/com/google/cloud/http/HttpTransportOptionsTest.java @@ -19,17 +19,53 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.HttpTesting; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.gax.rpc.HeaderProvider; +import com.google.api.gax.rpc.UnauthenticatedException; +import com.google.auth.Credentials; import com.google.auth.http.HttpTransportFactory; +import com.google.cloud.BaseService; +import com.google.cloud.NoCredentials; +import com.google.cloud.Service; +import com.google.cloud.ServiceDefaults; +import com.google.cloud.ServiceFactory; import com.google.cloud.ServiceOptions; +import com.google.cloud.ServiceRpc; +import com.google.cloud.TransportOptions; import com.google.cloud.http.HttpTransportOptions.DefaultHttpTransportFactory; +import com.google.cloud.spi.ServiceRpcFactory; +import java.io.IOException; +import java.util.HashMap; +import java.util.Set; import java.util.regex.Pattern; import org.easymock.EasyMock; +import org.junit.Before; import org.junit.Test; public class HttpTransportOptionsTest { + private static final HttpTransport MOCK_HTTP_TRANSPORT = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() { + return new MockLowLevelHttpResponse(); + } + }; + } + }; private static final HttpTransportFactory MOCK_HTTP_TRANSPORT_FACTORY = EasyMock.createMock(HttpTransportFactory.class); @@ -42,6 +78,35 @@ public class HttpTransportOptionsTest { private static final HttpTransportOptions DEFAULT_OPTIONS = HttpTransportOptions.newBuilder().build(); private static final HttpTransportOptions OPTIONS_COPY = OPTIONS.toBuilder().build(); + private static final String DEFAULT_PROJECT_ID = "testing"; + private static final String CUSTOM_UNIVERSE_DOMAIN = "random.com"; + + private HeaderProvider defaultHeaderProvider; + // Credentials' getUniverseDomain() returns GDU + private Credentials defaultCredentials; + // Credentials' getUniverseDomain() returns `random.com` + private Credentials customCredentials; + private HttpRequest defaultHttpRequest; + + @Before + public void setup() throws IOException { + defaultHeaderProvider = EasyMock.createMock(HeaderProvider.class); + EasyMock.expect(defaultHeaderProvider.getHeaders()).andReturn(new HashMap<>()); + + defaultCredentials = EasyMock.createMock(Credentials.class); + EasyMock.expect(defaultCredentials.getUniverseDomain()) + .andReturn(Credentials.GOOGLE_DEFAULT_UNIVERSE); + EasyMock.expect(defaultCredentials.hasRequestMetadata()).andReturn(false); + + customCredentials = EasyMock.createMock(Credentials.class); + EasyMock.expect(customCredentials.getUniverseDomain()).andReturn(CUSTOM_UNIVERSE_DOMAIN); + EasyMock.expect(customCredentials.hasRequestMetadata()).andReturn(false); + + EasyMock.replay(defaultHeaderProvider, defaultCredentials, customCredentials); + + defaultHttpRequest = + MOCK_HTTP_TRANSPORT.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL); + } @Test public void testBuilder() { @@ -78,4 +143,204 @@ public void testHeader() { .matcher(headerProvider.getHeaders().values().iterator().next()) .find()); } + + @Test + public void testHttpRequestInitializer_defaultUniverseDomainSettings_defaultCredentials() + throws IOException { + TestServiceOptions testServiceOptions = + generateTestServiceOptions(Credentials.GOOGLE_DEFAULT_UNIVERSE, defaultCredentials); + HttpRequestInitializer httpRequestInitializer = + DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions); + // Does not throw a validation exception + httpRequestInitializer.initialize(defaultHttpRequest); + } + + @Test + public void testHttpRequestInitializer_defaultUniverseDomainSettings_customCredentials() { + TestServiceOptions testServiceOptions = + generateTestServiceOptions(Credentials.GOOGLE_DEFAULT_UNIVERSE, customCredentials); + HttpRequestInitializer httpRequestInitializer = + DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions); + UnauthenticatedException exception = + assertThrows( + UnauthenticatedException.class, + () -> httpRequestInitializer.initialize(defaultHttpRequest)); + assertEquals( + "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.", + exception.getCause().getMessage()); + } + + @Test + public void testHttpRequestInitializer_customUniverseDomainSettings_defaultCredentials() { + TestServiceOptions testServiceOptions = + generateTestServiceOptions(CUSTOM_UNIVERSE_DOMAIN, defaultCredentials); + HttpRequestInitializer httpRequestInitializer = + DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions); + UnauthenticatedException exception = + assertThrows( + UnauthenticatedException.class, + () -> httpRequestInitializer.initialize(defaultHttpRequest)); + assertEquals( + "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.", + exception.getCause().getMessage()); + } + + @Test + public void testHttpRequestInitializer_customUniverseDomainSettings_customCredentials() + throws IOException { + TestServiceOptions testServiceOptions = + generateTestServiceOptions(CUSTOM_UNIVERSE_DOMAIN, customCredentials); + HttpRequestInitializer httpRequestInitializer = + DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions); + // Does not throw a validation exception + httpRequestInitializer.initialize(defaultHttpRequest); + } + + @Test + public void testHttpRequestInitializer_defaultUniverseDomainSettings_noCredentials() + throws IOException { + NoCredentials noCredentials = NoCredentials.getInstance(); + TestServiceOptions testServiceOptions = + generateTestServiceOptions(Credentials.GOOGLE_DEFAULT_UNIVERSE, noCredentials); + HttpRequestInitializer httpRequestInitializer = + DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions); + // Does not throw a validation exception + httpRequestInitializer.initialize(defaultHttpRequest); + } + + @Test + public void testHttpRequestInitializer_customUniverseDomainSettings_noCredentials() { + NoCredentials noCredentials = NoCredentials.getInstance(); + TestServiceOptions testServiceOptions = + generateTestServiceOptions(CUSTOM_UNIVERSE_DOMAIN, noCredentials); + HttpRequestInitializer httpRequestInitializer = + DEFAULT_OPTIONS.getHttpRequestInitializer(testServiceOptions); + UnauthenticatedException exception = + assertThrows( + UnauthenticatedException.class, + () -> httpRequestInitializer.initialize(defaultHttpRequest)); + assertEquals( + "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.", + exception.getCause().getMessage()); + } + + private TestServiceOptions generateTestServiceOptions( + String universeDomain, Credentials credentials) { + return TestServiceOptions.newBuilder() + .setCredentials(credentials) + .setHeaderProvider(defaultHeaderProvider) + .setQuotaProjectId(DEFAULT_PROJECT_ID) + .setProjectId(DEFAULT_PROJECT_ID) + .setUniverseDomain(universeDomain) + .build(); + } + + /** + * The following interfaces and classes are from ServiceOptionsTest. Copied over here as + * ServiceOptions resides inside google-cloud-core test folder and is not accessible from + * google-cloud-core-http. + */ + interface TestService extends Service {} + + private static class TestServiceImpl extends BaseService + implements TestService { + private TestServiceImpl(TestServiceOptions options) { + super(options); + } + } + + public interface TestServiceFactory extends ServiceFactory {} + + private static class DefaultTestServiceFactory implements TestServiceFactory { + private static final TestServiceFactory INSTANCE = new DefaultTestServiceFactory(); + + @Override + public TestService create(TestServiceOptions options) { + return new TestServiceImpl(options); + } + } + + public interface TestServiceRpcFactory extends ServiceRpcFactory {} + + private static class DefaultTestServiceRpcFactory implements TestServiceRpcFactory { + private static final TestServiceRpcFactory INSTANCE = new DefaultTestServiceRpcFactory(); + + @Override + public TestServiceRpc create(TestServiceOptions options) { + return new DefaultTestServiceRpc(options); + } + } + + private interface TestServiceRpc extends ServiceRpc {} + + private static class DefaultTestServiceRpc implements TestServiceRpc { + DefaultTestServiceRpc(TestServiceOptions options) {} + } + + static class TestServiceOptions extends ServiceOptions { + private static class Builder + extends ServiceOptions.Builder { + private Builder() {} + + private Builder(TestServiceOptions options) { + super(options); + } + + @Override + protected TestServiceOptions build() { + return new TestServiceOptions(this); + } + } + + private TestServiceOptions(Builder builder) { + super( + TestServiceFactory.class, + TestServiceRpcFactory.class, + builder, + new TestServiceDefaults()); + } + + private static class TestServiceDefaults + implements ServiceDefaults { + + @Override + public TestServiceFactory getDefaultServiceFactory() { + return DefaultTestServiceFactory.INSTANCE; + } + + @Override + public TestServiceRpcFactory getDefaultRpcFactory() { + return DefaultTestServiceRpcFactory.INSTANCE; + } + + @Override + public TransportOptions getDefaultTransportOptions() { + return new TransportOptions() {}; + } + } + + @Override + protected Set getScopes() { + return null; + } + + @Override + public Builder toBuilder() { + return new Builder(this); + } + + private static Builder newBuilder() { + return new Builder(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof TestServiceOptions && baseEquals((TestServiceOptions) obj); + } + + @Override + public int hashCode() { + return baseHashCode(); + } + } }