Skip to content
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

feat: Validate the Universe Domain inside Java-Core #2592

Merged
merged 13 commits into from
Apr 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
lqiu96 marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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<TestServiceOptions> {}

private static class TestServiceImpl extends BaseService<TestServiceOptions>
implements TestService {
private TestServiceImpl(TestServiceOptions options) {
super(options);
}
}

public interface TestServiceFactory extends ServiceFactory<TestService, TestServiceOptions> {}

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<TestServiceOptions> {}

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<TestService, TestServiceOptions> {
private static class Builder
extends ServiceOptions.Builder<TestService, TestServiceOptions, 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<TestService, TestServiceOptions> {

@Override
public TestServiceFactory getDefaultServiceFactory() {
return DefaultTestServiceFactory.INSTANCE;
}

@Override
public TestServiceRpcFactory getDefaultRpcFactory() {
return DefaultTestServiceRpcFactory.INSTANCE;
}

@Override
public TransportOptions getDefaultTransportOptions() {
return new TransportOptions() {};
}
}

@Override
protected Set<String> 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();
}
}
}
Loading