Skip to content

feat: support MySQL Automatic IAM Authentication #981

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

Merged
merged 9 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ jobs:
secrets: |-
MYSQL_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME
MYSQL_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER
MYSQL_IAM_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_JAVA_IAM_CONNECTION_NAME
MYSQL_IAM_USER_JAVA:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER_IAM_JAVA
MYSQL_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS
MYSQL_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB
POSTGRES_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME
Expand All @@ -97,6 +99,8 @@ jobs:
env:
MYSQL_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_CONNECTION_NAME }}'
MYSQL_USER: '${{ steps.secrets.outputs.MYSQL_USER }}'
MYSQL_IAM_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_IAM_CONNECTION_NAME }}'
MYSQL_IAM_USER: '${{ steps.secrets.outputs.MYSQL_IAM_USER_JAVA }}'
MYSQL_PASS: '${{ steps.secrets.outputs.MYSQL_PASS }}'
MYSQL_DB: '${{ steps.secrets.outputs.MYSQL_DB }}'
POSTGRES_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}'
Expand Down Expand Up @@ -181,6 +185,8 @@ jobs:
MYSQL_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER
MYSQL_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS
MYSQL_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB
MYSQL_IAM_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_JAVA_IAM_CONNECTION_NAME
MYSQL_IAM_USER_JAVA:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER_IAM_JAVA
POSTGRES_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME
POSTGRES_IAM_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_IAM_CONNECTION_NAME
POSTGRES_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER
Expand All @@ -197,6 +203,8 @@ jobs:
MYSQL_USER: '${{ steps.secrets.outputs.MYSQL_USER }}'
MYSQL_PASS: '${{ steps.secrets.outputs.MYSQL_PASS }}'
MYSQL_DB: '${{ steps.secrets.outputs.MYSQL_DB }}'
MYSQL_IAM_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_IAM_CONNECTION_NAME }}'
MYSQL_IAM_USER: '${{ steps.secrets.outputs.MYSQL_IAM_USER_JAVA }}'
POSTGRES_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}'
POSTGRES_IAM_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_IAM_CONNECTION_NAME }}'
POSTGRES_USER: '${{ steps.secrets.outputs.POSTGRES_USER }}'
Expand Down
36 changes: 35 additions & 1 deletion docs/jdbc-mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,40 @@ Note: The host portion of the JDBC URL is currently unused, and has no effect on

For more info on connecting using a private IP address, see [Requirements for Private IP](https://cloud.google.com/sql/docs/mysql/private-ip#requirements_for_private_ip).

### IAM Authentication
*Note:* This feature is currently only supported for MySQL j8 and Postgres drivers.
Connections using
[IAM database authentication](https://cloud.google.com/sql/docs/postgres/iam-logins)
are supported when connecting to Postgres instances.
This feature is unsupported for other drivers. First, make sure to
[configure your Cloud SQL Instance to allow IAM authentication](https://cloud.google.com/sql/docs/mysql/create-edit-iam-instances#configure-iam-db-instance)
and
[add an IAM database user](https://cloud.google.com/sql/docs/mysql/create-manage-iam-users#creating-a-database-user).
Now, you can connect using user or service
account credentials instead of a password.
When setting up the connection, set the `enableIamAuth` connection property to `"true"` and `user`
to the part of the email address associated with your IAM user before the @ symbol. For a service account, this is the service account's email address without the @project-id.iam.gserviceaccount.com suffix

Example:
```java
// Set up URL parameters
String jdbcURL = String.format("jdbc:mysql:///%s", DB_NAME);
Properties connProps = new Properties();
connProps.setProperty("user", "iam-user"); // [email protected]
connProps.setProperty("sslmode", "disable");
connProps.setProperty("socketFactory", "com.google.cloud.sql.postgres.SocketFactory");
connProps.setProperty("cloudSqlInstance", "project:region:instance");
connProps.setProperty("enableIamAuth", "true");

// Initialize connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcURL);
config.setDataSourceProperties(connProps);
config.setConnectionTimeout(10000); // 10s

HikariDataSource connectionPool = new HikariDataSource(config);
```

### Connection via Unix Sockets

To connect using a Unix domain socket (such as the one created by the Cloud SQL
Expand All @@ -74,4 +108,4 @@ Examples for using the Cloud SQL JDBC Connector for MySQL can be found by lookin
* [Connecting to Cloud SQL from App Engine Standard](https://cloud.google.com/sql/docs/mysql/connect-app-engine-standard)
* [Connecting to Cloud SQL from App Engine Flexible](https://cloud.google.com/sql/docs/mysql/connect-app-engine-flexible)
* [Connecting to Cloud SQL from Cloud Functions](https://cloud.google.com/sql/docs/mysql/connect-functions)
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/mysql/connect-run)
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/mysql/connect-run)
4 changes: 2 additions & 2 deletions docs/jdbc-postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Note: The host portion of the JDBC URL is currently unused, and has no effect on
For more info on connecting using a private IP address, see [Requirements for Private IP](https://cloud.google.com/sql/docs/mysql/private-ip#requirements_for_private_ip).

### IAM Authentication
*Note:* This feature is currently only supported for Postgres drivers.
*Note:* This feature is currently only supported for MySQL j8 and Postgres drivers.
Connections using
[IAM database authentication](https://cloud.google.com/sql/docs/postgres/iam-logins)
are supported when connecting to Postgres instances.
Expand Down Expand Up @@ -104,4 +104,4 @@ Examples for using the Cloud SQL JDBC Connector for Postgres can be found by loo
* [Connecting to Cloud SQL from App Engine Standard](https://cloud.google.com/sql/docs/postgres/connect-app-engine-standard)
* [Connecting to Cloud SQL from App Engine Flexible](https://cloud.google.com/sql/docs/postgres/connect-app-engine-flexible)
* [Connecting to Cloud SQL from Cloud Functions](https://cloud.google.com/sql/docs/postgres/connect-functions)
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/postgres/connect-run)
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/postgres/connect-run)
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright 2022 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.sql.mysql;


import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import com.google.common.collect.ImmutableList;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;


@RunWith(JUnit4.class)
public class JdbcMysqlJ8IamAuthIntegrationTests {

private static final String CONNECTION_NAME = System.getenv("MYSQL_IAM_CONNECTION_NAME");
private static final String DB_NAME = System.getenv("MYSQL_DB");
private static final String DB_USER = System.getenv("MYSQL_IAM_USER");

private static ImmutableList<String> requiredEnvVars = ImmutableList
.of("MYSQL_IAM_USER", "MYSQL_DB", "MYSQL_IAM_CONNECTION_NAME");
@Rule
public Timeout globalTimeout = new Timeout(30, TimeUnit.SECONDS);
private String tableName;
private HikariDataSource connectionPool;

@BeforeClass
public static void checkEnvVars() {
// Check that required env vars are set
requiredEnvVars.stream().forEach((varName) -> {
assertWithMessage(
String.format("Environment variable '%s' must be set to perform these tests.", varName))
.that(System.getenv(varName)).isNotEmpty();
});
}

@Before
public void setUpPool() throws SQLException {
// Set up URL parameters
String jdbcURL = String.format("jdbc:mysql:///%s", DB_NAME);
Properties connProps = new Properties();
connProps.setProperty("user", DB_USER);
connProps.setProperty("sslmode", "disable");
connProps.setProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory");
connProps.setProperty("cloudSqlInstance", CONNECTION_NAME);
connProps.setProperty("enableIamAuth", "true");

// Initialize connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcURL);
config.setDataSourceProperties(connProps);
config.setConnectionTimeout(10000); // 10s

this.connectionPool = new HikariDataSource(config);
this.tableName = String.format("books_%s", UUID.randomUUID().toString().replace("-", ""));

// Create table
try (Connection conn = connectionPool.getConnection()) {
String stmt = String.format("CREATE TABLE %s (", this.tableName)
+ " ID CHAR(20) NOT NULL,"
+ " TITLE TEXT NOT NULL"
+ ");";
try (PreparedStatement createTableStatement = conn.prepareStatement(stmt)) {
createTableStatement.execute();
}
}
}


@After
public void dropTableIfPresent() throws SQLException {
try (Connection conn = connectionPool.getConnection()) {
String stmt = String.format("DROP TABLE %s;", this.tableName);
try (PreparedStatement dropTableStatement = conn.prepareStatement(stmt)) {
dropTableStatement.execute();
}
}
}

@Test
public void pooledConnectionTest() throws SQLException {
try (Connection conn = connectionPool.getConnection()) {
String stmt = String.format("INSERT INTO %s (ID, TITLE) VALUES (?, ?)", this.tableName);
try (PreparedStatement insertStmt = conn.prepareStatement(stmt)) {
insertStmt.setQueryTimeout(10);
insertStmt.setString(1, "book1");
insertStmt.setString(2, "Book One");
insertStmt.execute();
insertStmt.setString(1, "book2");
insertStmt.setString(2, "Book Two");
insertStmt.execute();
}
}

List<String> bookList = new ArrayList<>();
try (Connection conn = connectionPool.getConnection()) {
String stmt = String.format("SELECT TITLE FROM %s ORDER BY ID", this.tableName);
try (PreparedStatement selectStmt = conn.prepareStatement(stmt)) {
selectStmt.setQueryTimeout(10); // 10s
ResultSet rs = selectStmt.executeQuery();
while (rs.next()) {
bookList.add(rs.getString("TITLE"));
}
}
}
assertThat(bookList).containsExactly("Book One", "Book Two");

}
}