Skip to content

Commit dc7d7ba

Browse files
authored
feat: support MySQL Automatic IAM Authentication (#981)
1 parent 8c54961 commit dc7d7ba

File tree

4 files changed

+186
-3
lines changed

4 files changed

+186
-3
lines changed

.github/workflows/tests.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ jobs:
8181
secrets: |-
8282
MYSQL_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME
8383
MYSQL_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER
84+
MYSQL_IAM_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_JAVA_IAM_CONNECTION_NAME
85+
MYSQL_IAM_USER_JAVA:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER_IAM_JAVA
8486
MYSQL_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS
8587
MYSQL_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB
8688
POSTGRES_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME
@@ -97,6 +99,8 @@ jobs:
9799
env:
98100
MYSQL_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_CONNECTION_NAME }}'
99101
MYSQL_USER: '${{ steps.secrets.outputs.MYSQL_USER }}'
102+
MYSQL_IAM_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_IAM_CONNECTION_NAME }}'
103+
MYSQL_IAM_USER: '${{ steps.secrets.outputs.MYSQL_IAM_USER_JAVA }}'
100104
MYSQL_PASS: '${{ steps.secrets.outputs.MYSQL_PASS }}'
101105
MYSQL_DB: '${{ steps.secrets.outputs.MYSQL_DB }}'
102106
POSTGRES_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}'
@@ -181,6 +185,8 @@ jobs:
181185
MYSQL_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER
182186
MYSQL_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS
183187
MYSQL_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB
188+
MYSQL_IAM_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_JAVA_IAM_CONNECTION_NAME
189+
MYSQL_IAM_USER_JAVA:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER_IAM_JAVA
184190
POSTGRES_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME
185191
POSTGRES_IAM_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_IAM_CONNECTION_NAME
186192
POSTGRES_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER
@@ -197,6 +203,8 @@ jobs:
197203
MYSQL_USER: '${{ steps.secrets.outputs.MYSQL_USER }}'
198204
MYSQL_PASS: '${{ steps.secrets.outputs.MYSQL_PASS }}'
199205
MYSQL_DB: '${{ steps.secrets.outputs.MYSQL_DB }}'
206+
MYSQL_IAM_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_IAM_CONNECTION_NAME }}'
207+
MYSQL_IAM_USER: '${{ steps.secrets.outputs.MYSQL_IAM_USER_JAVA }}'
200208
POSTGRES_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}'
201209
POSTGRES_IAM_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_IAM_CONNECTION_NAME }}'
202210
POSTGRES_USER: '${{ steps.secrets.outputs.POSTGRES_USER }}'

docs/jdbc-mysql.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,40 @@ Note: The host portion of the JDBC URL is currently unused, and has no effect on
5454

5555
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).
5656

57+
### IAM Authentication
58+
*Note:* This feature is currently only supported for MySQL j8 and Postgres drivers.
59+
Connections using
60+
[IAM database authentication](https://cloud.google.com/sql/docs/postgres/iam-logins)
61+
are supported when connecting to Postgres instances.
62+
This feature is unsupported for other drivers. First, make sure to
63+
[configure your Cloud SQL Instance to allow IAM authentication](https://cloud.google.com/sql/docs/mysql/create-edit-iam-instances#configure-iam-db-instance)
64+
and
65+
[add an IAM database user](https://cloud.google.com/sql/docs/mysql/create-manage-iam-users#creating-a-database-user).
66+
Now, you can connect using user or service
67+
account credentials instead of a password.
68+
When setting up the connection, set the `enableIamAuth` connection property to `"true"` and `user`
69+
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
70+
71+
Example:
72+
```java
73+
// Set up URL parameters
74+
String jdbcURL = String.format("jdbc:mysql:///%s", DB_NAME);
75+
Properties connProps = new Properties();
76+
connProps.setProperty("user", "iam-user"); // [email protected]
77+
connProps.setProperty("sslmode", "disable");
78+
connProps.setProperty("socketFactory", "com.google.cloud.sql.postgres.SocketFactory");
79+
connProps.setProperty("cloudSqlInstance", "project:region:instance");
80+
connProps.setProperty("enableIamAuth", "true");
81+
82+
// Initialize connection pool
83+
HikariConfig config = new HikariConfig();
84+
config.setJdbcUrl(jdbcURL);
85+
config.setDataSourceProperties(connProps);
86+
config.setConnectionTimeout(10000); // 10s
87+
88+
HikariDataSource connectionPool = new HikariDataSource(config);
89+
```
90+
5791
### Connection via Unix Sockets
5892

5993
To connect using a Unix domain socket (such as the one created by the Cloud SQL
@@ -74,4 +108,4 @@ Examples for using the Cloud SQL JDBC Connector for MySQL can be found by lookin
74108
* [Connecting to Cloud SQL from App Engine Standard](https://cloud.google.com/sql/docs/mysql/connect-app-engine-standard)
75109
* [Connecting to Cloud SQL from App Engine Flexible](https://cloud.google.com/sql/docs/mysql/connect-app-engine-flexible)
76110
* [Connecting to Cloud SQL from Cloud Functions](https://cloud.google.com/sql/docs/mysql/connect-functions)
77-
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/mysql/connect-run)
111+
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/mysql/connect-run)

docs/jdbc-postgres.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Note: The host portion of the JDBC URL is currently unused, and has no effect on
4747
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).
4848

4949
### IAM Authentication
50-
*Note:* This feature is currently only supported for Postgres drivers.
50+
*Note:* This feature is currently only supported for MySQL j8 and Postgres drivers.
5151
Connections using
5252
[IAM database authentication](https://cloud.google.com/sql/docs/postgres/iam-logins)
5353
are supported when connecting to Postgres instances.
@@ -104,4 +104,4 @@ Examples for using the Cloud SQL JDBC Connector for Postgres can be found by loo
104104
* [Connecting to Cloud SQL from App Engine Standard](https://cloud.google.com/sql/docs/postgres/connect-app-engine-standard)
105105
* [Connecting to Cloud SQL from App Engine Flexible](https://cloud.google.com/sql/docs/postgres/connect-app-engine-flexible)
106106
* [Connecting to Cloud SQL from Cloud Functions](https://cloud.google.com/sql/docs/postgres/connect-functions)
107-
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/postgres/connect-run)
107+
* [Connecting to Cloud SQL from Cloud Run](https://cloud.google.com/sql/docs/postgres/connect-run)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.sql.mysql;
18+
19+
20+
import static com.google.common.truth.Truth.assertThat;
21+
import static com.google.common.truth.Truth.assertWithMessage;
22+
23+
import com.google.common.collect.ImmutableList;
24+
import com.zaxxer.hikari.HikariConfig;
25+
import com.zaxxer.hikari.HikariDataSource;
26+
import java.sql.Connection;
27+
import java.sql.PreparedStatement;
28+
import java.sql.ResultSet;
29+
import java.sql.SQLException;
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.Properties;
33+
import java.util.UUID;
34+
import java.util.concurrent.TimeUnit;
35+
import org.junit.After;
36+
import org.junit.Before;
37+
import org.junit.BeforeClass;
38+
import org.junit.Rule;
39+
import org.junit.Test;
40+
import org.junit.rules.Timeout;
41+
import org.junit.runner.RunWith;
42+
import org.junit.runners.JUnit4;
43+
44+
45+
@RunWith(JUnit4.class)
46+
public class JdbcMysqlJ8IamAuthIntegrationTests {
47+
48+
private static final String CONNECTION_NAME = System.getenv("MYSQL_IAM_CONNECTION_NAME");
49+
private static final String DB_NAME = System.getenv("MYSQL_DB");
50+
private static final String DB_USER = System.getenv("MYSQL_IAM_USER");
51+
52+
private static ImmutableList<String> requiredEnvVars = ImmutableList
53+
.of("MYSQL_IAM_USER", "MYSQL_DB", "MYSQL_IAM_CONNECTION_NAME");
54+
@Rule
55+
public Timeout globalTimeout = new Timeout(30, TimeUnit.SECONDS);
56+
private String tableName;
57+
private HikariDataSource connectionPool;
58+
59+
@BeforeClass
60+
public static void checkEnvVars() {
61+
// Check that required env vars are set
62+
requiredEnvVars.stream().forEach((varName) -> {
63+
assertWithMessage(
64+
String.format("Environment variable '%s' must be set to perform these tests.", varName))
65+
.that(System.getenv(varName)).isNotEmpty();
66+
});
67+
}
68+
69+
@Before
70+
public void setUpPool() throws SQLException {
71+
// Set up URL parameters
72+
String jdbcURL = String.format("jdbc:mysql:///%s", DB_NAME);
73+
Properties connProps = new Properties();
74+
connProps.setProperty("user", DB_USER);
75+
connProps.setProperty("sslmode", "disable");
76+
connProps.setProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory");
77+
connProps.setProperty("cloudSqlInstance", CONNECTION_NAME);
78+
connProps.setProperty("enableIamAuth", "true");
79+
80+
// Initialize connection pool
81+
HikariConfig config = new HikariConfig();
82+
config.setJdbcUrl(jdbcURL);
83+
config.setDataSourceProperties(connProps);
84+
config.setConnectionTimeout(10000); // 10s
85+
86+
this.connectionPool = new HikariDataSource(config);
87+
this.tableName = String.format("books_%s", UUID.randomUUID().toString().replace("-", ""));
88+
89+
// Create table
90+
try (Connection conn = connectionPool.getConnection()) {
91+
String stmt = String.format("CREATE TABLE %s (", this.tableName)
92+
+ " ID CHAR(20) NOT NULL,"
93+
+ " TITLE TEXT NOT NULL"
94+
+ ");";
95+
try (PreparedStatement createTableStatement = conn.prepareStatement(stmt)) {
96+
createTableStatement.execute();
97+
}
98+
}
99+
}
100+
101+
102+
@After
103+
public void dropTableIfPresent() throws SQLException {
104+
try (Connection conn = connectionPool.getConnection()) {
105+
String stmt = String.format("DROP TABLE %s;", this.tableName);
106+
try (PreparedStatement dropTableStatement = conn.prepareStatement(stmt)) {
107+
dropTableStatement.execute();
108+
}
109+
}
110+
}
111+
112+
@Test
113+
public void pooledConnectionTest() throws SQLException {
114+
try (Connection conn = connectionPool.getConnection()) {
115+
String stmt = String.format("INSERT INTO %s (ID, TITLE) VALUES (?, ?)", this.tableName);
116+
try (PreparedStatement insertStmt = conn.prepareStatement(stmt)) {
117+
insertStmt.setQueryTimeout(10);
118+
insertStmt.setString(1, "book1");
119+
insertStmt.setString(2, "Book One");
120+
insertStmt.execute();
121+
insertStmt.setString(1, "book2");
122+
insertStmt.setString(2, "Book Two");
123+
insertStmt.execute();
124+
}
125+
}
126+
127+
List<String> bookList = new ArrayList<>();
128+
try (Connection conn = connectionPool.getConnection()) {
129+
String stmt = String.format("SELECT TITLE FROM %s ORDER BY ID", this.tableName);
130+
try (PreparedStatement selectStmt = conn.prepareStatement(stmt)) {
131+
selectStmt.setQueryTimeout(10); // 10s
132+
ResultSet rs = selectStmt.executeQuery();
133+
while (rs.next()) {
134+
bookList.add(rs.getString("TITLE"));
135+
}
136+
}
137+
}
138+
assertThat(bookList).containsExactly("Book One", "Book Two");
139+
140+
}
141+
}

0 commit comments

Comments
 (0)