diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8a48fac4..7d439fb1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -30,6 +30,10 @@ permissions: read-all
jobs:
units:
runs-on: ubuntu-latest
+ permissions:
+ contents: "read"
+ id-token: "write"
+ issues: write
strategy:
fail-fast: false
matrix:
@@ -43,12 +47,21 @@ jobs:
with:
distribution: zulu
java-version: ${{matrix.java}}
+ - uses: google-github-actions/auth@35b0e87d162680511bf346c299f71c9c5c379033 # v1.1.1
+ with:
+ workload_identity_provider: ${{ secrets.PROVIDER_NAME }}
+ service_account: ${{ secrets.SERVICE_ACCOUNT }}
+ access_token_lifetime: 600s
- run: java -version
- run: .kokoro/build.sh
env:
JOB_TYPE: test
windows:
runs-on: windows-latest
+ permissions:
+ contents: "read"
+ id-token: "write"
+ issues: write
steps:
- name: Support longpaths
run: git config --system core.longpaths true
@@ -60,6 +73,11 @@ jobs:
with:
distribution: zulu
java-version: 8
+ - uses: google-github-actions/auth@35b0e87d162680511bf346c299f71c9c5c379033 # v1.1.1
+ with:
+ workload_identity_provider: ${{ secrets.PROVIDER_NAME }}
+ service_account: ${{ secrets.SERVICE_ACCOUNT }}
+ access_token_lifetime: 600s
- run: java -version
- run: .kokoro/build.bat
env:
diff --git a/alloydb-jdbc-connector/pom.xml b/alloydb-jdbc-connector/pom.xml
index 9fc6b18c..eb9b6679 100644
--- a/alloydb-jdbc-connector/pom.xml
+++ b/alloydb-jdbc-connector/pom.xml
@@ -111,6 +111,12 @@
google-auth-library-oauth2-http
+
+ com.google.auth
+ google-auth-library-credentials
+ provided
+
+
org.slf4j
diff --git a/alloydb-jdbc-connector/src/test/java/com/google/cloud/alloydb/InternalConnectorRegistryTest.java b/alloydb-jdbc-connector/src/test/java/com/google/cloud/alloydb/InternalConnectorRegistryTest.java
new file mode 100644
index 00000000..4a3b6331
--- /dev/null
+++ b/alloydb-jdbc-connector/src/test/java/com/google/cloud/alloydb/InternalConnectorRegistryTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.alloydb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.alloydb.v1.InstanceName;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Properties;
+import org.junit.Test;
+
+public class InternalConnectorRegistryTest {
+
+ private static final String INSTANCE_NAME =
+ "projects//locations//clusters//instances/";
+
+ @Test
+ public void create_failOnInvalidInstanceName() throws IOException {
+ try {
+ InternalConnectorRegistry.INSTANCE.connect(
+ new ConnectionConfig.Builder().withInstanceName(InstanceName.parse("myProject")).build());
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .contains("InstanceName.parse: formattedString not in valid format");
+ }
+ }
+
+ @Test
+ public void create_failOnEmptyTargetPrincipal() throws IOException, InterruptedException {
+ try {
+ InternalConnectorRegistry.INSTANCE.connect(
+ new ConnectionConfig.Builder()
+ .withInstanceName(InstanceName.parse(INSTANCE_NAME))
+ .withConnectorConfig(
+ new ConnectorConfig.Builder()
+ .withDelegates(
+ Collections.singletonList("delegate-service-principal@example.com"))
+ .build())
+ .build());
+ fail("IllegalArgumentException expected.");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains(ConnectionConfig.ALLOYDB_TARGET_PRINCIPAL);
+ }
+ }
+
+ @Test
+ public void registerConnection_failWithDuplicateName() throws InterruptedException {
+ // Register a Connection
+ String namedConnector = "my-connection-name";
+ ConnectorConfig configWithDetails = new ConnectorConfig.Builder().build();
+ InternalConnectorRegistry.INSTANCE.register(namedConnector, configWithDetails);
+
+ // Assert that you can't register a connection with a duplicate name
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> InternalConnectorRegistry.INSTANCE.register(namedConnector, configWithDetails));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains(String.format("Named connection %s exists.", namedConnector));
+ }
+
+ @Test
+ public void registerConnection_failWithDuplicateNameAndDifferentConfig()
+ throws InterruptedException {
+ String namedConnector = "test-connection";
+ ConnectorConfig config =
+ new ConnectorConfig.Builder().withTargetPrincipal("joe@test.com").build();
+ InternalConnectorRegistry.INSTANCE.register(namedConnector, config);
+
+ ConnectorConfig config2 =
+ new ConnectorConfig.Builder().withTargetPrincipal("jane@test.com").build();
+
+ // Assert that you can't register a connection with a duplicate name
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> InternalConnectorRegistry.INSTANCE.register(namedConnector, config2));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains(String.format("Named connection %s exists.", namedConnector));
+ }
+
+ @Test
+ public void closeNamedConnection_failWhenNotFound() throws InterruptedException {
+ String namedConnector = "a-connection";
+ // Assert that you can't close a connection that doesn't exist
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> InternalConnectorRegistry.INSTANCE.close(namedConnector));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains(String.format("Named connection %s does not exist.", namedConnector));
+ }
+
+ @Test
+ public void connect_failOnClosedNamedConnection() throws InterruptedException {
+ // Register a Connection
+ String namedConnector = "my-connection";
+ ConnectorConfig configWithDetails = new ConnectorConfig.Builder().build();
+ InternalConnectorRegistry.INSTANCE.register(namedConnector, configWithDetails);
+
+ // Close the named connection.
+ InternalConnectorRegistry.INSTANCE.close(namedConnector);
+
+ // Attempt and fail to connect using the cloudSqlNamedConnection connection property
+ Properties connProps = new Properties();
+ connProps.setProperty(ConnectionConfig.ALLOYDB_NAMED_CONNECTOR, namedConnector);
+ ConnectionConfig nameOnlyConfig = ConnectionConfig.fromConnectionProperties(connProps);
+
+ // Assert that no connection is possible because the connector is closed.
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> InternalConnectorRegistry.INSTANCE.connect(nameOnlyConfig));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains(String.format("Named connection %s does not exist.", namedConnector));
+ }
+
+ @Test
+ public void connect_failOnUnknownNamedConnection() throws InterruptedException {
+ // Attempt and fail to connect using the Named Connection not registered
+ String namedConnector = "first-connection";
+ Properties connProps = new Properties();
+ connProps.setProperty(ConnectionConfig.ALLOYDB_NAMED_CONNECTOR, namedConnector);
+ ConnectionConfig nameOnlyConfig = ConnectionConfig.fromConnectionProperties(connProps);
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> InternalConnectorRegistry.INSTANCE.connect(nameOnlyConfig));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains(String.format("Named connection %s does not exist.", namedConnector));
+ }
+
+ @Test
+ public void connect_failOnNamedConnectionAfterResetInstance() throws InterruptedException {
+ // Register a Connection
+ String namedConnector = "this-connection";
+ ConnectorConfig config = new ConnectorConfig.Builder().build();
+ Properties connProps = new Properties();
+ connProps.setProperty(ConnectionConfig.ALLOYDB_INSTANCE_NAME, INSTANCE_NAME);
+ connProps.setProperty(ConnectionConfig.ALLOYDB_NAMED_CONNECTOR, namedConnector);
+ ConnectionConfig nameOnlyConfig = ConnectionConfig.fromConnectionProperties(connProps);
+
+ InternalConnectorRegistry.INSTANCE.register(namedConnector, config);
+
+ InternalConnectorRegistry.INSTANCE.resetInstance();
+
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> InternalConnectorRegistry.INSTANCE.connect(nameOnlyConfig));
+ assertThat(ex)
+ .hasMessageThat()
+ .contains(String.format("Named connection %s does not exist.", namedConnector));
+ }
+}
diff --git a/alloydb-jdbc-connector/src/test/java/com/google/cloud/alloydb/ServiceAccountImpersonatingCredentialFactoryTest.java b/alloydb-jdbc-connector/src/test/java/com/google/cloud/alloydb/ServiceAccountImpersonatingCredentialFactoryTest.java
new file mode 100644
index 00000000..f2a9533c
--- /dev/null
+++ b/alloydb-jdbc-connector/src/test/java/com/google/cloud/alloydb/ServiceAccountImpersonatingCredentialFactoryTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.alloydb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.ImpersonatedCredentials;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Test;
+
+public class ServiceAccountImpersonatingCredentialFactoryTest {
+
+ @Test
+ public void testImpersonatedCredentialsWithMultipleAccounts() {
+ DefaultCredentialFactory factory = new DefaultCredentialFactory();
+ Credentials credentials = factory.getCredentials();
+
+ CredentialFactory impersonatedFactory =
+ new ServiceAccountImpersonatingCredentialFactory(
+ factory,
+ "first@serviceaccount.com",
+ Arrays.asList("third@serviceaccount.com", "second@serviceaccount.com"));
+
+ // Test that the CredentialsFactory.create() works.
+ Credentials impersonatedCredentials = impersonatedFactory.getCredentials();
+ assertThat(impersonatedCredentials).isInstanceOf(ImpersonatedCredentials.class);
+
+ // Test that CredentialsFactory.getCredentials() works.
+ assertThat(impersonatedFactory.getCredentials()).isInstanceOf(ImpersonatedCredentials.class);
+ ImpersonatedCredentials ic = (ImpersonatedCredentials) impersonatedFactory.getCredentials();
+ assertThat(ic.getAccount()).isEqualTo("first@serviceaccount.com");
+ assertThat(ic.getSourceCredentials()).isEqualTo(credentials);
+ }
+
+ @Test
+ public void testImpersonatedCredentialsWithOneAccount() {
+ DefaultCredentialFactory factory = new DefaultCredentialFactory();
+ Credentials credentials = factory.getCredentials();
+
+ CredentialFactory impersonatedFactory =
+ new ServiceAccountImpersonatingCredentialFactory(factory, "first@serviceaccount.com", null);
+
+ // Test that the CredentialsFactory.create() works.
+ Credentials impersonatedCredentials = impersonatedFactory.getCredentials();
+ assertThat(impersonatedCredentials).isInstanceOf(ImpersonatedCredentials.class);
+
+ // Test that CredentialsFactory.getCredentials() works.
+ assertThat(impersonatedFactory.getCredentials()).isInstanceOf(ImpersonatedCredentials.class);
+ ImpersonatedCredentials ic = (ImpersonatedCredentials) impersonatedFactory.getCredentials();
+ assertThat(ic.getAccount()).isEqualTo("first@serviceaccount.com");
+ assertThat(ic.getSourceCredentials()).isEqualTo(credentials);
+ }
+
+ @Test
+ public void testEmptyDelegatesThrowsIllegalArgumentException() {
+ DefaultCredentialFactory factory = new DefaultCredentialFactory();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ new ServiceAccountImpersonatingCredentialFactory(factory, null, Collections.emptyList());
+ });
+ }
+}