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()); + }); + } +}