Skip to content

Commit fe59516

Browse files
feat: set hide params to true postgres engine [DIA-86641] (#398)
1 parent bbbb878 commit fe59516

4 files changed

Lines changed: 108 additions & 8 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,24 @@ And define the environment variable `sqlalchemy_url` with `postgres+asyncpg` sch
149149
export sqlalchemy_url=postgresql+asyncpg://postgres@localhost
150150
```
151151

152+
### Hiding SQL parameters
153+
154+
By default, `hide_parameters` is set to `True` on all engines to prevent SQL query
155+
parameters from appearing in error messages and logs. This helps protect sensitive data
156+
such as PII/PHI.
157+
158+
To disable this (e.g. for local debugging or dev):
159+
160+
```bash
161+
export sqlalchemy_hide_parameters=false
162+
```
163+
164+
For named sessions:
165+
166+
```bash
167+
export fastapi_sqla__read_only__sqlalchemy_hide_parameters=false
168+
```
169+
152170
## Setup the app AsyncContextManager (recommended):
153171

154172
```python

fastapi_sqla/async_sqla.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os
21
from collections.abc import AsyncGenerator
32
from contextlib import asynccontextmanager
43
from typing import Annotated, Union
@@ -17,7 +16,12 @@
1716
from starlette.types import ASGIApp, Message, Receive, Scope, Send
1817

1918
from fastapi_sqla import aws_aurora_support, aws_rds_iam_support
20-
from fastapi_sqla.sqla import _DEFAULT_SESSION_KEY, Base, get_envvar_prefix
19+
from fastapi_sqla.sqla import (
20+
_DEFAULT_SESSION_KEY,
21+
Base,
22+
_get_engine_config,
23+
get_envvar_prefix,
24+
)
2125

2226
logger = structlog.get_logger(__name__)
2327

@@ -29,9 +33,8 @@ def new_async_engine(
2933
key: str = _DEFAULT_SESSION_KEY,
3034
) -> Union[AsyncEngine, AsyncConnection]:
3135
envvar_prefix = get_envvar_prefix(key)
32-
lowercase_environ = {k.lower(): v for k, v in os.environ.items()}
33-
lowercase_environ.pop(f"{envvar_prefix}warn_20", None)
34-
return async_engine_from_config(lowercase_environ, prefix=envvar_prefix)
36+
config = _get_engine_config(envvar_prefix)
37+
return async_engine_from_config(config, prefix=envvar_prefix)
3538

3639

3740
async def startup(key: str = _DEFAULT_SESSION_KEY):

fastapi_sqla/sqla.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from fastapi import Depends, Request, Response
99
from fastapi.concurrency import contextmanager_in_threadpool
1010
from fastapi.responses import PlainTextResponse
11+
from pydantic import BaseModel
1112
from sqlalchemy import engine_from_config, text
1213
from sqlalchemy.engine import Connection, Engine
1314
from sqlalchemy.ext.declarative import DeferredReflection
@@ -38,6 +39,15 @@
3839
_session_factories: dict[str, sessionmaker] = {}
3940

4041

42+
class _EngineConfig(BaseModel):
43+
"""Engine configuration with typed defaults and bool coercion."""
44+
45+
hide_parameters: bool = True
46+
47+
class Config:
48+
extra = "allow"
49+
50+
4151
class Base(DeclarativeBase, DeferredReflection):
4252
__abstract__ = True
4353

@@ -50,11 +60,31 @@ def get_envvar_prefix(key: str) -> str:
5060
return envvar_prefix
5161

5262

63+
def _get_engine_config(
64+
envvar_prefix: str,
65+
) -> dict[str, Union[str, bool]]:
66+
"""Build engine config dict with opinionated defaults and type coercion."""
67+
lowercase_env: dict[str, Union[str, bool]] = {
68+
k.lower(): v for k, v in os.environ.items()
69+
}
70+
lowercase_env.pop(f"{envvar_prefix}warn_20", None)
71+
72+
overrides = {
73+
k[len(envvar_prefix) :]: v
74+
for k, v in lowercase_env.items()
75+
if k.startswith(envvar_prefix)
76+
}
77+
config = _EngineConfig(**overrides) # type: ignore[arg-type]
78+
for param, value in config.dict().items():
79+
lowercase_env[f"{envvar_prefix}{param}"] = value
80+
81+
return lowercase_env
82+
83+
5384
def new_engine(key: str = _DEFAULT_SESSION_KEY) -> Union[Engine, Connection]:
5485
envvar_prefix = get_envvar_prefix(key)
55-
lowercase_environ = {k.lower(): v for k, v in os.environ.items()}
56-
lowercase_environ.pop(f"{envvar_prefix}warn_20", None)
57-
return engine_from_config(lowercase_environ, prefix=envvar_prefix)
86+
config = _get_engine_config(envvar_prefix)
87+
return engine_from_config(config, prefix=envvar_prefix)
5888

5989

6090
def startup(key: str = _DEFAULT_SESSION_KEY):

tests/test_sqla_startup.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,52 @@ def now():
190190
res = await client.get("/one")
191191

192192
assert res.json() == 1
193+
194+
195+
def test_new_engine_hide_parameters_default(monkeypatch):
196+
from fastapi_sqla.sqla import new_engine
197+
198+
monkeypatch.delenv("sqlalchemy_hide_parameters", raising=False)
199+
200+
engine_or_conn = new_engine()
201+
202+
assert engine_or_conn.engine.hide_parameters is True
203+
204+
205+
def test_new_engine_hide_parameters_opt_out(monkeypatch):
206+
from fastapi_sqla.sqla import new_engine
207+
208+
monkeypatch.setenv("sqlalchemy_hide_parameters", "false")
209+
210+
engine_or_conn = new_engine()
211+
212+
assert engine_or_conn.engine.hide_parameters is False
213+
214+
215+
@mark.require_asyncpg
216+
@mark.sqlalchemy("1.4")
217+
async def test_new_async_engine_hide_parameters_default(monkeypatch, async_session_key):
218+
from fastapi_sqla.async_sqla import new_async_engine
219+
220+
monkeypatch.delenv(
221+
f"fastapi_sqla__{async_session_key}__sqlalchemy_hide_parameters",
222+
raising=False,
223+
)
224+
225+
engine_or_conn = new_async_engine(async_session_key)
226+
227+
assert engine_or_conn.sync_engine.hide_parameters is True
228+
229+
230+
@mark.require_asyncpg
231+
@mark.sqlalchemy("1.4")
232+
async def test_new_async_engine_hide_parameters_opt_out(monkeypatch, async_session_key):
233+
from fastapi_sqla.async_sqla import new_async_engine
234+
235+
monkeypatch.setenv(
236+
f"fastapi_sqla__{async_session_key}__sqlalchemy_hide_parameters", "false"
237+
)
238+
239+
engine_or_conn = new_async_engine(async_session_key)
240+
241+
assert engine_or_conn.sync_engine.hide_parameters is False

0 commit comments

Comments
 (0)