Skip to content

Commit a11e646

Browse files
authored
feat: support AWS IAM authentication for RDS (DIA-39107) (#43)
1 parent d8566b1 commit a11e646

10 files changed

Lines changed: 283 additions & 16 deletions

File tree

.circleci/config.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ workflows:
2424
parameters:
2525
sqlalchemy_version: ["1.3", "1.4"]
2626
asyncpg: ["asyncpg", "noasyncpg"]
27+
aws_rds_iam: ["aws_rds_iam", "noaws_rds_iam"]
2728
- release/release:
2829
name: release
2930
requires:
@@ -62,13 +63,17 @@ jobs:
6263
type: enum
6364
enum: ["asyncpg", "noasyncpg"]
6465
description: To run tests with and without asyncpg installed.
66+
aws_rds_iam:
67+
type: enum
68+
enum: ["aws_rds_iam", "noaws_rds_iam"]
69+
description: To run tests with and without asyncpg installed.
6570
executor: python-postgres
6671
working_directory: ~/project/.
6772
steps:
6873
- base/setup
6974
- python/setup
7075
- utils/with_cache:
71-
key: 'sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }}'
76+
key: 'sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>-<<parameters.aws_rds_iam>>-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }}'
7277
namespace: tox
7378
path: ~/project/.tox
7479
steps:
@@ -77,13 +82,13 @@ jobs:
7782
command: |
7883
poetry run pip install -U tox
7984
- run:
80-
name: "run tox using sqlalchemy <<parameters.sqlalchemy_version>>.* and -<<parameters.asyncpg>>"
85+
name: "run tox using sqlalchemy <<parameters.sqlalchemy_version>>.* and -<<parameters.asyncpg>> and -<<parameters.aws_rds_iam>>"
8186
command: |
82-
poetry run tox -e sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>
87+
poetry run tox -e sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>-<<parameters.aws_rds_iam>>
8388
- store_test_results:
8489
path: test-reports
8590
- utils/send_coverage_to_codecov:
86-
codecov_flag: sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>
91+
codecov_flag: sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>-<<parameters.aws_rds_iam>>
8792

8893
publish:
8994
docker:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ It returns the path of `alembic.ini` configuration file. By default, it returns
406406
## Setup
407407

408408
```bash
409-
$ poetry install --extras tests --extras asyncpg
409+
$ poetry install --extras tests --extras asyncpg --extras aws_rds_iam
410410
```
411411

412412
## Running tests

fastapi_sqla/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from sqlalchemy.orm.session import sessionmaker
1919
from sqlalchemy.sql import Select, func, select
2020

21+
from . import aws_rds_iam_support
22+
2123
try:
2224
from sqlalchemy.orm import declarative_base
2325
except ImportError:
@@ -63,6 +65,8 @@ def setup(app: FastAPI):
6365
def startup():
6466
lowercase_environ = {k.lower(): v for k, v in os.environ.items()}
6567
engine = engine_from_config(lowercase_environ, prefix="sqlalchemy_")
68+
aws_rds_iam_support.setup(engine.engine)
69+
6670
Base.metadata.bind = engine
6771
Base.prepare(engine)
6872
_Session.configure(bind=engine)

fastapi_sqla/_pytest_plugin.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,33 @@
1717

1818

1919
@fixture(scope="session")
20-
def db_url():
21-
"""Default db url used by depending fixtures.
20+
def db_host():
21+
"""Default db host used by depending fixtures.
22+
23+
When CI key is set in environment variables, it uses `postgres` as host name else, host used is `localhost`
24+
"""
25+
26+
return "postgres" if "CI" in os.environ else "localhost"
27+
28+
29+
@fixture(scope="session")
30+
def db_user():
31+
"""Default db user used by depending fixtures.
2232
23-
When CI key is set in environment variables, it uses `postgres` as host name:
24-
postgresql://postgres@posgres/postgres
33+
postgres
34+
"""
35+
36+
return "postgres"
2537

26-
Else, host used is `localhost`: postgresql://postgres@localhost/postgres
38+
39+
@fixture(scope="session")
40+
def db_url(db_host, db_user):
41+
"""Default db url used by depending fixtures.
42+
43+
db url example postgresql://{db_user}@{db_host}/postgres
2744
"""
28-
host = "postgres" if "CI" in os.environ else "localhost"
29-
return f"postgresql://postgres@{host}/postgres"
45+
46+
return f"postgresql://{db_user}@{db_host}/postgres"
3047

3148

3249
@fixture(scope="session")

fastapi_sqla/asyncio_support.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from sqlalchemy.ext.asyncio import create_async_engine
99
from sqlalchemy.orm.session import sessionmaker
1010

11+
from . import aws_rds_iam_support
12+
1113
logger = structlog.get_logger(__name__)
1214
_ASYNC_SESSION_KEY = "fastapi_sqla_async_session"
1315
_AsyncSession = sessionmaker(class_=SqlaAsyncSession)
@@ -16,6 +18,7 @@
1618
async def startup():
1719
async_sqlalchemy_url = os.environ["async_sqlalchemy_url"]
1820
engine = create_async_engine(async_sqlalchemy_url)
21+
aws_rds_iam_support.setup(engine.sync_engine)
1922
_AsyncSession.configure(bind=engine, expire_on_commit=False)
2023

2124
# Fail early:
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
try:
2+
import boto3
3+
4+
boto3_installed = True
5+
except ImportError as err:
6+
boto3_installed = False
7+
boto3_installed_err = str(err)
8+
9+
from pydantic import BaseSettings
10+
from sqlalchemy import event
11+
12+
13+
def setup(engine):
14+
config = Config()
15+
16+
if config.aws_rds_iam_enabled:
17+
assert boto3_installed, boto3_installed_err
18+
event.listen(engine, "do_connect", set_connection_token)
19+
20+
21+
def get_authentication_token(host, port, user):
22+
session = boto3.Session()
23+
client = session.client("rds")
24+
token = client.generate_db_auth_token(DBHostname=host, Port=port, DBUsername=user)
25+
return token
26+
27+
28+
def set_connection_token(dialect, conn_rec, cargs, cparams):
29+
cparams["password"] = get_authentication_token(
30+
host=cparams["host"], port=cparams.get("port", 5432), user=cparams["user"]
31+
)
32+
33+
34+
class Config(BaseSettings):
35+
aws_rds_iam_enabled: bool = False
36+
37+
class Config:
38+
env_prefix = "fastapi_sqla_"

0 commit comments

Comments
 (0)