Skip to content

Commit c217fcb

Browse files
authored
Merge pull request #354 from DataIntegrationGroup/secure-db
feat: update Cloud SQL authentication to support IAM and refactor connection logic
2 parents 6306cc3 + afd0995 commit c217fcb

3 files changed

Lines changed: 59 additions & 24 deletions

File tree

.github/workflows/CD_staging.yml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ jobs:
3535
- name: Authenticate to Google Cloud
3636
uses: 'google-github-actions/auth@v2'
3737
with:
38-
credentials_json: ${{ secrets.CLOUD_SQL_SERVICE_ACCOUNT_KEY }}
38+
credentials_json: ${{ secrets.CLOUD_DEPLOY_SERVICE_ACCOUNT_KEY }}
3939

4040
- name: Run Alembic migrations on staging database
4141
env:
4242
DB_DRIVER: "cloudsql"
4343
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
4444
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
4545
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
46-
CLOUD_SQL_PASSWORD: "${{ secrets.CLOUD_SQL_PASSWORD }}"
46+
CLOUD_SQL_IAM_AUTH: true
4747
run: |
4848
uv run alembic upgrade head
4949
@@ -53,17 +53,12 @@ jobs:
5353
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
5454
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
5555
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
56-
CLOUD_SQL_PASSWORD: "${{ secrets.CLOUD_SQL_PASSWORD }}"
56+
CLOUD_SQL_IAM_AUTH: true
5757
GCS_SERVICE_ACCOUNT_KEY: "${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}"
5858
GCS_BUCKET_NAME: "${{ vars.GCS_BUCKET_NAME }}"
5959
run: |
6060
uv run python -m transfers.backfill.staging
6161
62-
- name: Authenticate to Google Cloud
63-
uses: 'google-github-actions/auth@v2'
64-
with:
65-
credentials_json: ${{ secrets.CLOUD_DEPLOY_SERVICE_ACCOUNT_KEY }}
66-
6762
# Uses Google Cloud Secret Manager to store secret credentials
6863
- name: Create app.yaml
6964
run: |
@@ -82,7 +77,7 @@ jobs:
8277
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
8378
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
8479
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
85-
CLOUD_SQL_PASSWORD: "${{ secrets.CLOUD_SQL_PASSWORD }}"
80+
CLOUD_SQL_IAM_AUTH: true
8681
GCS_SERVICE_ACCOUNT_KEY: "${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}"
8782
GCS_BUCKET_NAME: "${{ vars.GCS_BUCKET_NAME }}"
8883
AUTHENTIK_URL: "${{ vars.AUTHENTIK_URL }}"

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ reset_db.sh
2929
tests/uploads
3030
migrate.sh
3131
launcher.sh
32-
gcs_credentials.json
32+
*credentials.json
3333
transfers/data/assets*
3434
transfers/data/nma_csv_cache/*
3535
transfers/data/*.csv

db/engine.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# ===============================================================================
1616

1717
import asyncio
18+
import copy
1819
import getpass
1920
import os
2021
from contextlib import contextmanager
@@ -29,10 +30,32 @@
2930
)
3031
from sqlalchemy.util import await_only
3132

33+
from services.util import get_bool_env
34+
3235
load_dotenv()
3336
driver = os.environ.get("DB_DRIVER", "")
3437

3538

39+
def get_iam_login_token() -> str:
40+
"""
41+
Return a short-lived IAM DB auth token for Cloud SQL Postgres.
42+
"""
43+
from google.auth import default
44+
from google.auth.transport.requests import Request
45+
46+
scopes = ["https://www.googleapis.com/auth/sqlservice.login"]
47+
creds, _ = default()
48+
if hasattr(creds, "with_scopes"):
49+
creds = creds.with_scopes(scopes=scopes)
50+
else:
51+
creds = copy.copy(creds)
52+
creds._scopes = scopes # type: ignore[attr-defined]
53+
creds.refresh(Request())
54+
if not getattr(creds, "token", None):
55+
raise RuntimeError("Unable to acquire IAM DB auth token.")
56+
return creds.token
57+
58+
3659
async def get_async_engine():
3760
"""
3861
Asynchronous database session generator.
@@ -48,14 +71,21 @@ def asyncify_connection():
4871
user = os.environ.get("CLOUD_SQL_USER")
4972
password = os.environ.get("CLOUD_SQL_PASSWORD")
5073
database = os.environ.get("CLOUD_SQL_DATABASE")
51-
52-
connection = connector.connect_async(
53-
instance_name,
54-
"asyncpg",
55-
db=database,
56-
password=password,
57-
user=user,
58-
)
74+
use_iam_auth = get_bool_env("CLOUD_SQL_IAM_AUTH", False)
75+
ip_type = os.environ.get("CLOUD_SQL_IP_TYPE", "public")
76+
77+
connect_kwargs = {
78+
"db": database,
79+
"user": user,
80+
"enable_iam_auth": use_iam_auth,
81+
"ip_type": ip_type,
82+
}
83+
if use_iam_auth:
84+
connect_kwargs["password"] = get_iam_login_token()
85+
else:
86+
connect_kwargs["password"] = password
87+
88+
connection = connector.connect_async(instance_name, "asyncpg", **connect_kwargs)
5989

6090
return AsyncAdapt_asyncpg_connection(
6191
engine.dialect.dbapi,
@@ -78,15 +108,25 @@ def init_connection_pool(connector):
78108
user = os.environ.get("CLOUD_SQL_USER")
79109
password = os.environ.get("CLOUD_SQL_PASSWORD")
80110
database = os.environ.get("CLOUD_SQL_DATABASE")
111+
use_iam_auth = get_bool_env("CLOUD_SQL_IAM_AUTH", False)
112+
ip_type = os.environ.get("CLOUD_SQL_IP_TYPE", "public")
81113

82114
def getconn():
115+
connect_kwargs = {
116+
"user": user,
117+
"db": database,
118+
"ip_type": ip_type,
119+
"enable_iam_auth": use_iam_auth,
120+
}
121+
if use_iam_auth:
122+
connect_kwargs["password"] = get_iam_login_token()
123+
else:
124+
connect_kwargs["password"] = password
125+
83126
conn = connector.connect(
84127
instance_name, # The Cloud SQL instance name
85128
"pg8000",
86-
user=user,
87-
password=password,
88-
db=database,
89-
ip_type="public",
129+
**connect_kwargs,
90130
)
91131
return conn
92132

@@ -107,7 +147,7 @@ def getconn():
107147
connector = Connector()
108148
engine = init_connection_pool(connector)
109149

110-
async_engine = asyncio.run(get_async_engine())
150+
# async_engine = asyncio.run(get_async_engine())
111151

112152
else:
113153
# if driver == "sqlite":
@@ -161,7 +201,7 @@ def getconn():
161201
# listen(engine, "connect", on_connect)
162202

163203

164-
async_database_sessionmaker = async_sessionmaker(async_engine)
204+
# async_database_sessionmaker = async_sessionmaker(async_engine)
165205
database_sessionmaker = sessionmaker(engine, expire_on_commit=False)
166206

167207

0 commit comments

Comments
 (0)