From a8283284390a809436f289e6670f4979b3951828 Mon Sep 17 00:00:00 2001 From: "Tobias.Mikula" Date: Tue, 29 Sep 2026 15:18:12 +0200 Subject: [PATCH] feat(database): Exercise least-privilege db roles --- README.md | 4 ++-- src/utils/postgres_base.py | 7 ++++++- tests/integration/conftest.py | 33 +++++++++++++++++++++------------ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index dc4e68a..1df3b49 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Environment variables: - `TRACE_REDACT_KEYS` (optional) – comma separated message keys redacted from `TRACE` payload logs. Defaults to `password,secret,token,key,apikey,api_key`. - `TRACE_MAX_BYTES` (optional) – maximum size of a logged `TRACE` payload. Defaults to `10000`. - `CONF_DIR` (optional) – directory containing `config.json` and `access.json`. Defaults to `conf`. -- `POSTGRES_SECRET_NAME` (optional) – AWS Secrets Manager secret name holding PostgreSQL connection credentials (host, port, database, user, password). Required for Postgres writer and stats reader. +- `POSTGRES_SECRET_NAME` (optional) – AWS Secrets Manager secret name holding PostgreSQL connection credentials (host, port, database, user, password). Each Lambda should point at its own least-privilege role's secret (see `database/README.md` for the role/grant reference). - `POSTGRES_SECRET_REGION` (optional) – AWS region of the Secrets Manager secret. Must be set together with `POSTGRES_SECRET_NAME`. ## Logging & Correlation @@ -206,7 +206,7 @@ Configured via `kafka_bootstrap_server`. (Future: support auth properties / TLS Publishes events to the configured `event_bus_arn` using put events API. ### Postgres Writer -Writes enriched event data (runs + jobs) into PostgreSQL tables. Connection credentials are loaded from AWS Secrets Manager using the `POSTGRES_SECRET_NAME` and `POSTGRES_SECRET_REGION` environment variables. +Writes enriched event data (runs + jobs) into PostgreSQL tables. Connection credentials are loaded from AWS Secrets Manager using the `POSTGRES_SECRET_NAME` and `POSTGRES_SECRET_REGION` environment variables, which should reference the `eventgate_writer` role's secret (see `database/README.md`). ## Troubleshooting | Symptom | Possible Cause | Action | diff --git a/src/utils/postgres_base.py b/src/utils/postgres_base.py index 9b1ae5a..64dd9cc 100644 --- a/src/utils/postgres_base.py +++ b/src/utils/postgres_base.py @@ -129,7 +129,12 @@ def _get_connection(self) -> Any: self._connection = psycopg2.connect(**connect_kwargs) logger.info( "New PostgreSQL connection established.", - extra={"database": pg_config["database"], "host": pg_config["host"], "port": pg_config["port"]}, + extra={ + "database": pg_config["database"], + "host": pg_config["host"], + "port": pg_config["port"], + "user": pg_config["user"], + }, ) return self._connection diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 3dfe71a..18ac60d 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -311,23 +311,30 @@ def lambda_handler_factory( os.environ["LOG_LEVEL"] = "DEBUG" os.environ["AWS_DEFAULT_REGION"] = mock_aws_services["region"] - # Store PostgreSQL credentials in mocked Secrets Manager so WriterPostgres can find them. + # Store PostgreSQL credentials in mocked Secrets Manager so WriterPostgres/ReaderPostgres can + # find them. Mirror production: each Lambda gets its own secret for its own least-privilege + # role (eventgate_writer / eventgate_reader), created by Flyway's V1.4.0.1__create_roles.ddl. parsed_dsn = urlparse(postgres_container) - pg_secret = { - "database": parsed_dsn.path.lstrip("/"), - "host": parsed_dsn.hostname, - "port": parsed_dsn.port, - "user": parsed_dsn.username, - "password": parsed_dsn.password, - } + + def _pg_secret_for_role(role_user: str) -> str: + secret = { + "database": parsed_dsn.path.lstrip("/"), + "host": parsed_dsn.hostname, + "port": parsed_dsn.port, + "user": role_user, + "password": TEST_ROLE_PASSWORD, + } + return json.dumps(secret) + sm_client = boto3.client( "secretsmanager", region_name=mock_aws_services["region"], ) - sm_client.create_secret(Name="eventgate/postgres", SecretString=json.dumps(pg_secret)) - os.environ["POSTGRES_SECRET_NAME"] = "eventgate/postgres" + sm_client.create_secret(Name="eventgate/writer", SecretString=_pg_secret_for_role("eventgate_writer")) + sm_client.create_secret(Name="eventgate/reader", SecretString=_pg_secret_for_role("eventgate_reader")) os.environ["POSTGRES_SECRET_REGION"] = mock_aws_services["region"] - logger.debug("PostgreSQL secret stored in moto Secrets Manager.") + os.environ["POSTGRES_SECRET_NAME"] = "eventgate/writer" + logger.debug("PostgreSQL writer/reader secrets stored in moto Secrets Manager.") # Create test config with container URLs. test_config_dir = PROJECT_ROOT / "tests" / "integration" / ".tmp_conf" @@ -406,7 +413,9 @@ def stats_lambda_handler( ``lambda_handler_factory`` to trigger that setup, then import the stats Lambda module. """ - # lambda_handler_factory has already set up the env; import stats Lambda. + # lambda_handler_factory has already set up the env, but pointed POSTGRES_SECRET_NAME at the + # eventgate_writer secret for WriterPostgres, so changing it now for the eventgate_reader instead. + os.environ["POSTGRES_SECRET_NAME"] = "eventgate/reader" from src.event_stats_lambda import lambda_handler as stats_handler return stats_handler