Skip to content

Commit 80644d7

Browse files
feat: add support for MySQL auto IAM AuthN (#466)
1 parent 212f3a4 commit 80644d7

File tree

10 files changed

+113
-26
lines changed

10 files changed

+113
-26
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ jobs:
8383
with:
8484
secrets: |-
8585
MYSQL_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME
86+
MYSQL_IAM_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_IAM_CONNECTION_NAME
8687
MYSQL_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER
88+
MYSQL_IAM_USER:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_USER_IAM_PYTHON
8789
MYSQL_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_PASS
8890
MYSQL_DB:${{ secrets.GOOGLE_CLOUD_PROJECT }}/MYSQL_DB
8991
POSTGRES_CONNECTION_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CONNECTION_NAME
@@ -100,7 +102,9 @@ jobs:
100102
- name: Run tests
101103
env:
102104
MYSQL_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_CONNECTION_NAME }}'
105+
MYSQL_IAM_CONNECTION_NAME: '${{ steps.secrets.outputs.MYSQL_IAM_CONNECTION_NAME }}'
103106
MYSQL_USER: '${{ steps.secrets.outputs.MYSQL_USER }}'
107+
MYSQL_IAM_USER: '${{ steps.secrets.outputs.MYSQL_IAM_USER }}'
104108
MYSQL_PASS: '${{ steps.secrets.outputs.MYSQL_PASS }}'
105109
MYSQL_DB: '${{ steps.secrets.outputs.MYSQL_DB }}'
106110
POSTGRES_CONNECTION_NAME: '${{ steps.secrets.outputs.POSTGRES_CONNECTION_NAME }}'

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,16 @@ connector.connect(
242242
Note: If specifying Private IP, your application must already be in the same VPC network as your Cloud SQL Instance.
243243

244244
### IAM Authentication
245-
Connections using [Automatic IAM database authentication](https://cloud.google.com/sql/docs/postgres/authentication#automatic) are supported when using the Postgres driver. This feature is unsupported for other drivers. If automatic IAM authentication is not supported for your driver, you can use [Manual IAM database authentication](https://cloud.google.com/sql/docs/postgres/authentication#manual) to connect.
246-
First, make sure to [configure your Cloud SQL Instance to allow IAM authentication](https://cloud.google.com/sql/docs/postgres/create-edit-iam-instances#configure-iam-db-instance) and [add an IAM database user](https://cloud.google.com/sql/docs/postgres/create-manage-iam-users#creating-a-database-user).
245+
Connections using [Automatic IAM database authentication](https://cloud.google.com/sql/docs/postgres/authentication#automatic) are supported when using Postgres or MySQL drivers.
246+
First, make sure to [configure your Cloud SQL Instance to allow IAM authentication](https://cloud.google.com/sql/docs/postgres/create-edit-iam-instances#configure-iam-db-instance)
247+
and [add an IAM database user](https://cloud.google.com/sql/docs/postgres/create-manage-iam-users#creating-a-database-user).
248+
247249
Now, you can connect using user or service account credentials instead of a password.
248-
In the call to connect, set the `enable_iam_auth` keyword argument to true and `user` to the email address associated with your IAM user.
250+
In the call to connect, set the `enable_iam_auth` keyword argument to true and the `user` argument to the appropriately formatted IAM principal.
251+
> Postgres: For an IAM user account, this is the user's email address. For a service account, it is the service account's email without the `.gserviceaccount.com` domain suffix.
252+
253+
> MySQL: For an IAM user account, this is the user's email address, without the @ or domain name. For example, for `[email protected]`, set the `user` argument to `test-user`. For a service account, this is the service account's email address without the `@project-id.iam.gserviceaccount.com` suffix.
254+
249255
Example:
250256
```python
251257
connector.connect(

google/cloud/sql/connector/connector.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ class Connector:
4646
4747
:type enable_iam_auth: bool
4848
:param enable_iam_auth
49-
Enables IAM based authentication (Postgres only).
49+
Enables automatic IAM database authentication for Postgres or MySQL
50+
instances.
5051
5152
:type timeout: int
5253
:param timeout
@@ -319,7 +320,8 @@ async def create_async_connector(
319320
320321
:type enable_iam_auth: bool
321322
:param enable_iam_auth
322-
Enables IAM based authentication (Postgres only).
323+
Enables automatic IAM database authentication for Postgres or MySQL
324+
instances.
323325
324326
:type timeout: int
325327
:param timeout

google/cloud/sql/connector/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class CredentialsTypeError(Exception):
5959
class AutoIAMAuthNotSupported(Exception):
6060
"""
6161
Exception to be raised when Automatic IAM Authentication is not
62-
supported with database engine version.f
62+
supported with database engine version.
6363
"""
6464

6565
pass

google/cloud/sql/connector/instance.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ class Instance:
143143
If not specified, Application Default Credentials are used.
144144
145145
:param enable_iam_auth
146-
Enables IAM based authentication for Postgres instances.
146+
Enables automatic IAM database authentication for Postgres or MySQL
147+
instances.
147148
:type enable_iam_auth: bool
148149
149150
:param loop:
@@ -328,9 +329,9 @@ async def _perform_refresh(self) -> InstanceMetadata:
328329
# check if automatic IAM database authn is supported for database engine
329330
if self._enable_iam_auth and not metadata[
330331
"database_version"
331-
].startswith("POSTGRES"):
332+
].startswith(("POSTGRES", "MYSQL")):
332333
raise AutoIAMAuthNotSupported(
333-
f"'{metadata['database_version']}' does not support automatic IAM authentication. It is only supported with Cloud SQL Postgres instances."
334+
f"'{metadata['database_version']}' does not support automatic IAM authentication. It is only supported with Cloud SQL Postgres or MySQL instances."
334335
)
335336
except Exception:
336337
# cancel ephemeral cert task if exception occurs before it is awaited

google/cloud/sql/connector/pymysql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def connect(
4646
'Unable to import module "pymysql." Please install and try again.'
4747
)
4848

49+
# allow automatic IAM database authentication to not require password
50+
kwargs["password"] = kwargs["password"] if "password" in kwargs else None
51+
4952
# Create socket and wrap with context.
5053
sock = ctx.wrap_socket(
5154
socket.create_connection((ip_address, SERVER_PROXY_PORT)),

google/cloud/sql/connector/refresh_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ async def _get_ephemeral(
144144
145145
:type enable_iam_auth: bool
146146
:param enable_iam_auth
147-
Enables IAM based authentication for Postgres instances.
147+
Enables automatic IAM database authentication for Postgres or MySQL
148+
instances.
148149
149150
:rtype: str
150151
:returns: An ephemeral certificate from the Cloud SQL instance that allows

tests/system/test_connector_object.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -145,21 +145,6 @@ def test_connector_with_custom_loop() -> None:
145145
assert connector._thread is None
146146

147147

148-
def test_connector_mysql_iam_auth_error() -> None:
149-
"""
150-
Test that connecting with enable_iam_auth set to True
151-
for MySQL raises exception.
152-
"""
153-
with pytest.raises(AutoIAMAuthNotSupported):
154-
with Connector(enable_iam_auth=True) as connector:
155-
connector.connect(
156-
os.environ["MYSQL_CONNECTION_NAME"],
157-
"pymysql",
158-
user="my-user",
159-
db="my-db",
160-
)
161-
162-
163148
def test_connector_sqlserver_iam_auth_error() -> None:
164149
"""
165150
Test that connecting with enable_iam_auth set to True
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
import os
17+
import uuid
18+
from typing import Generator
19+
20+
import pytest
21+
import pymysql
22+
import sqlalchemy
23+
from google.cloud.sql.connector import Connector
24+
25+
table_name = f"books_{uuid.uuid4().hex}"
26+
27+
28+
# [START cloud_sql_connector_mysql_pymysql_iam_auth]
29+
# The Cloud SQL Python Connector can be used along with SQLAlchemy using the
30+
# 'creator' argument to 'create_engine'
31+
def init_connection_engine() -> sqlalchemy.engine.Engine:
32+
def getconn() -> pymysql.connections.Connection:
33+
# initialize Connector object for connections to Cloud SQL
34+
with Connector() as connector:
35+
conn: pymysql.connections.Connection = connector.connect(
36+
os.environ["MYSQL_IAM_CONNECTION_NAME"],
37+
"pymysql",
38+
user=os.environ["MYSQL_IAM_USER"],
39+
db=os.environ["MYSQL_DB"],
40+
enable_iam_auth=True,
41+
)
42+
return conn
43+
44+
# create SQLAlchemy connection pool
45+
pool = sqlalchemy.create_engine(
46+
"mysql+pymysql://",
47+
creator=getconn,
48+
)
49+
return pool
50+
51+
52+
# [END cloud_sql_connector_mysql_pymysql_iam_auth]
53+
54+
55+
@pytest.fixture(name="pool")
56+
def setup() -> Generator:
57+
pool = init_connection_engine()
58+
59+
with pool.connect() as conn:
60+
conn.execute(
61+
f"CREATE TABLE IF NOT EXISTS {table_name}"
62+
" ( id CHAR(20) NOT NULL, title TEXT NOT NULL );"
63+
)
64+
65+
yield pool
66+
67+
with pool.connect() as conn:
68+
conn.execute(f"DROP TABLE IF EXISTS {table_name}")
69+
70+
71+
def test_pooled_connection_with_pymysql_iam_auth(
72+
pool: sqlalchemy.engine.Engine,
73+
) -> None:
74+
insert_stmt = sqlalchemy.text(
75+
f"INSERT INTO {table_name} (id, title) VALUES (:id, :title)",
76+
)
77+
with pool.connect() as conn:
78+
conn.execute(insert_stmt, id="book1", title="Book One")
79+
conn.execute(insert_stmt, id="book2", title="Book Two")
80+
81+
select_stmt = sqlalchemy.text(f"SELECT title FROM {table_name} ORDER BY ID;")
82+
with pool.connect() as conn:
83+
rows = conn.execute(select_stmt).fetchall()
84+
titles = [row[0] for row in rows]
85+
86+
assert titles == ["Book One", "Book Two"]

tests/unit/test_instance.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,6 @@ async def test_ClientResponseError(
414414
"mock_instance",
415415
[
416416
mocks.FakeCSQLInstance(db_version="SQLSERVER_2019_STANDARD"),
417-
mocks.FakeCSQLInstance(db_version="MYSQL_8_0"),
418417
],
419418
)
420419
async def test_AutoIAMAuthNotSupportedError(instance: Instance) -> None:

0 commit comments

Comments
 (0)