Skip to content

Commit

Permalink
feat: Validate the Universe Domain inside Java-Core (#2592)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
lqiu96 authored Apr 12, 2024
1 parent ad641e5 commit 35d789f
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 2 deletions.
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() {
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();
}
}
}

0 comments on commit 35d789f

Please sign in to comment.