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: Add Universe Domain to Java-Core #2329

Merged
merged 24 commits into from Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -81,7 +81,6 @@ public abstract class ServiceOptions<
implements Serializable {

public static final String CREDENTIAL_ENV_NAME = "GOOGLE_APPLICATION_CREDENTIALS";

private static final String DEFAULT_HOST = "https://www.googleapis.com";
private static final String LEGACY_PROJECT_ENV_NAME = "GCLOUD_PROJECT";
private static final String PROJECT_ENV_NAME = "GOOGLE_CLOUD_PROJECT";
Expand All @@ -95,6 +94,7 @@ public abstract class ServiceOptions<
protected final String clientLibToken;

private final String projectId;
private final String universeDomain;
private final String host;
private final RetrySettings retrySettings;
private final String serviceRpcFactoryClassName;
Expand Down Expand Up @@ -125,6 +125,7 @@ public abstract static class Builder<
private final ImmutableSet<String> allowedClientLibTokens =
ImmutableSet.of(ServiceOptions.getGoogApiClientLibName());
private String projectId;
private String universeDomain;
private String host;
protected Credentials credentials;
private RetrySettings retrySettings;
Expand All @@ -142,6 +143,7 @@ protected Builder() {}
@InternalApi("This class should only be extended within google-cloud-java")
protected Builder(ServiceOptions<ServiceT, OptionsT> options) {
projectId = options.projectId;
universeDomain = options.universeDomain;
host = options.host;
credentials = options.credentials;
retrySettings = options.retrySettings;
Expand Down Expand Up @@ -199,6 +201,22 @@ public B setHost(String host) {
return self();
}

/**
* Universe Domain is the domain for Google Cloud Services. A Google Cloud endpoint follows the
* format of `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a
* Universe Domain value of `googleapis.com` and cloudasset.test.com would have a Universe
* Domain of `test.com`.
*
* <p>If this value is not set, the resolved UniverseDomain will default to `googleapis.com`.
*
* @throws NullPointerException if {@code universeDomain} is {@code null}. The resolved
* universeDomain will be `googleapis.com` if this value is not set.
*/
public B setUniverseDomain(String universeDomain) {
this.universeDomain = checkNotNull(universeDomain);
return self();
}

/**
* Sets the service authentication credentials. If no credentials are set, {@link
* GoogleCredentials#getApplicationDefault()} will be used to attempt getting credentials from
Expand Down Expand Up @@ -306,6 +324,7 @@ protected ServiceOptions(
"A project ID is required for this service but could not be determined from the builder "
+ "or the environment. Please set a project ID using the builder.");
}
universeDomain = builder.universeDomain;
host = firstNonNull(builder.host, getDefaultHost());
credentials = builder.credentials != null ? builder.credentials : defaultCredentials();
retrySettings = firstNonNull(builder.retrySettings, getDefaultRetrySettings());
Expand Down Expand Up @@ -582,6 +601,19 @@ public String getProjectId() {
return projectId;
}

/**
* Universe Domain is the domain for Google Cloud Services. A Google Cloud endpoint follows the
* format of `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a
* Universe Domain value of `googleapis.com` and cloudasset.test.com would have a Universe Domain
* of `test.com`.
*
* @return The universe domain value set in the Builder's setter. This is not the resolved
* Universe Domain
*/
public String getUniverseDomain() {
return universeDomain;
}

/** Returns the service host. */
public String getHost() {
return host;
Expand Down Expand Up @@ -767,4 +799,62 @@ public String getClientLibToken() {
public String getQuotaProjectId() {
return quotaProjectId;
}

/**
* Returns the resolved host for the Service to connect to Google Cloud
*
* <p>The resolved host will be in `https://{serviceName}.{resolvedUniverseDomain}` format. The
* resolvedUniverseDomain will be set to `googleapis.com` if universeDomain is null. The format is
* similar to the DEFAULT_HOST value in java-core.
*
* @see <a
* href="https://github.com/googleapis/sdk-platform-java/blob/097964f24fa1989bc74b4807a253f0be4e9dd1ea/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java#L85">DEFAULT_HOST</a>
*/
@InternalApi
public String getResolvedHost(String serviceName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling it as host is itchy. Fine but ensure to explain this to library owners.
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URL.html#getHost()

if (universeDomain != null && universeDomain.isEmpty()) {
throw new IllegalArgumentException("The universe domain cannot be empty");
}
String resolvedUniverseDomain =
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
// The host value set to DEFAULT_HOST if the user didn't configure a host. If the
// user set a host the library uses that value, otherwise, construct the host for the user.
// The DEFAULT_HOST value is not a valid host for handwritten libraries and should be
// overriden to include the serviceName.
if (!DEFAULT_HOST.equals(host)) {
return host;
blakeli0 marked this conversation as resolved.
Show resolved Hide resolved
}
return "https://" + serviceName + "." + resolvedUniverseDomain;
}

/**
* Temporarily used for BigQuery and Storage Apiary Wrapped Libraries. To be removed in the future
* when Apiary clients can resolve their endpoints. Returns the host to be used as the rootUrl.
*
* <p>The resolved host will be in `https://{serviceName}.{resolvedUniverseDomain}/` format. The
* resolvedUniverseDomain will be set to `googleapis.com` if universeDomain is null.
*
* @see <a
* href="https://github.com/googleapis/google-api-java-client/blob/76765d5f9689be9d266a7d62fa6ffb4cabf701f5/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClient.java#L49">rootUrl</a>
*/
@InternalApi
public String getResolvedApiaryHost(String serviceName) {
String resolvedUniverseDomain =
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
return "https://" + serviceName + "." + resolvedUniverseDomain + "/";
}

/**
* Validates that Credentials' Universe Domain matches the resolved Universe Domain. Currently,
* this is only intended for BigQuery and Storage Apiary Wrapped Libraries.
*
* <p>This validation call should be made prior to any RPC invocation. This call is used to gate
* the RPC invocation if there is no valid universe domain.
*/
@InternalApi
public boolean hasValidUniverseDomain() throws IOException {
String resolvedUniverseDomain =
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
return resolvedUniverseDomain.equals(getCredentials().getUniverseDomain());
}
}
Expand Up @@ -22,6 +22,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

Expand Down Expand Up @@ -55,6 +56,7 @@ public class ServiceOptionsTest {
private static GoogleCredentials credentials;
private static GoogleCredentials credentialsWithProjectId;
private static GoogleCredentials credentialsWithQuotaProject;
private static GoogleCredentials credentialsNotInGDU;

private static final String JSON_KEY =
"{\n"
Expand All @@ -81,7 +83,8 @@ public class ServiceOptionsTest {
+ "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n"
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\"\n"
+ " \"type\": \"service_account\",\n"
+ " \"universe_domain\": \"googleapis.com\"\n"
+ "}";

private static final String JSON_KEY_PROJECT_ID =
Expand Down Expand Up @@ -110,7 +113,8 @@ public class ServiceOptionsTest {
+ " \"project_id\": \"someprojectid\",\n"
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\"\n"
+ " \"type\": \"service_account\",\n"
+ " \"universe_domain\": \"googleapis.com\"\n"
+ "}";

private static final String JSON_KEY_QUOTA_PROJECT_ID =
Expand Down Expand Up @@ -140,13 +144,45 @@ public class ServiceOptionsTest {
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\",\n"
+ " \"quota_project_id\": \"some-quota-project-id\"\n"
+ " \"quota_project_id\": \"some-quota-project-id\",\n"
+ " \"universe_domain\": \"googleapis.com\"\n"
+ "}";

// Key added by copying the keys above and adding in the universe domain field
private static final String JSON_KEY_NON_GDU =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you add source code comment how you generated this value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't generate this value. I copied it from above and added a new json entry for universe_domain. This universe_domain addition to Credentials was part of Auth v1.21.0

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add that "copied it from above and added a new json entry for universe_domain"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

"{\n"
+ " \"private_key_id\": \"somekeyid\",\n"
+ " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS"
+ "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg"
+ "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4"
+ "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2"
+ "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa"
+ "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF"
+ "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL"
+ "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\"
+ "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp"
+ "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF"
+ "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm"
+ "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK"
+ "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF"
+ "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR"
+ "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl"
+ "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa"
+ "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi"
+ "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG"
+ "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk"
+ "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n"
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\",\n"
+ " \"universe_domain\": \"random.com\"\n"
+ "}";

static {
credentials = loadCredentials(JSON_KEY);
credentialsWithProjectId = loadCredentials(JSON_KEY_PROJECT_ID);
credentialsWithQuotaProject = loadCredentials(JSON_KEY_QUOTA_PROJECT_ID);
credentialsNotInGDU = loadCredentials(JSON_KEY_NON_GDU);
}

static GoogleCredentials loadCredentials(String credentialFile) {
Expand Down Expand Up @@ -471,6 +507,129 @@ public void testResponseHeaderDoesNotContainMetaDataFlavor() throws Exception {
assertThat(ServiceOptions.headerContainsMetadataFlavor(httpResponse)).isFalse();
}

@Test
public void testGetResolvedEndpoint_noUniverseDomain() {
TestServiceOptions options = TestServiceOptions.newBuilder().setProjectId("project-id").build();
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.googleapis.com");
}

@Test
public void testGetResolvedEndpoint_emptyUniverseDomain() {
TestServiceOptions options =
TestServiceOptions.newBuilder().setUniverseDomain("").setProjectId("project-id").build();
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> options.getResolvedHost("service"));
assertThat(exception.getMessage()).isEqualTo("The universe domain cannot be empty");
}

@Test
public void testGetResolvedEndpoint_customUniverseDomain() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setProjectId("project-id")
.build();
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.test.com");
}

@Test
public void testGetResolvedEndpoint_customUniverseDomain_customHost() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setHost("https://service.random.com/")
.setProjectId("project-id")
.build();
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.random.com/");
}

@Test
public void testGetResolvedApiaryHost_noUniverseDomain() {
TestServiceOptions options = TestServiceOptions.newBuilder().setProjectId("project-id").build();
assertThat(options.getResolvedApiaryHost("service"))
.isEqualTo("https://service.googleapis.com/");
}

@Test
public void testGetResolvedApiaryHost_customUniverseDomain_noHost() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setHost(null)
.setProjectId("project-id")
.build();
assertThat(options.getResolvedApiaryHost("service")).isEqualTo("https://service.test.com/");
}

@Test
public void testGetResolvedApiaryHost_customUniverseDomain_customHost() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setHost("https://service.random.com")
.setProjectId("project-id")
.build();
assertThat(options.getResolvedApiaryHost("service")).isEqualTo("https://service.test.com/");
}

// No User Configuration = GDU, Default Credentials = GDU
@Test
public void testIsValidUniverseDomain_noUserUniverseDomainConfig_defaultCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setCredentials(credentials)
.build();
assertThat(options.hasValidUniverseDomain()).isTrue();
}

// No User Configuration = GDU, non Default Credentials = random.com
// non-GDU Credentials could be any domain, the tests use random.com
@Test
public void testIsValidUniverseDomain_noUserUniverseDomainConfig_nonGDUCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setCredentials(credentialsNotInGDU)
.build();
assertThat(options.hasValidUniverseDomain()).isFalse();
}

// User Configuration = random.com, Default Credentials = GDU
// User Credentials could be set to any domain, the tests use random.com
@Test
public void testIsValidUniverseDomain_userUniverseDomainConfig_defaultCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setUniverseDomain("random.com")
.setCredentials(credentials)
.build();
assertThat(options.hasValidUniverseDomain()).isFalse();
}

// User Configuration = random.com, non Default Credentials = random.com
// User Credentials and non GDU Credentials could be set to any domain,
// the tests use random.com
@Test
public void testIsValidUniverseDomain_userUniverseDomainConfig_nonGDUCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setUniverseDomain("random.com")
.setCredentials(credentialsNotInGDU)
.build();
assertThat(options.hasValidUniverseDomain()).isTrue();
}

private HttpResponse createHttpResponseWithHeader(final Multimap<String, String> headers)
throws Exception {
HttpTransport mockHttpTransport =
Expand Down