From e35a0d4f0d23e07a6fad26e5c89b57533ec10bfd Mon Sep 17 00:00:00 2001 From: SashkoMarchuk Date: Thu, 2 Apr 2026 15:03:47 +0700 Subject: [PATCH 1/6] infra(cpb): add database, user, and env vars for Connecting People Bot - .env.example: CPB variables for dev and prod sections - .env: CPB dev defaults (using Reporter Bot token for testing) - docker-compose.yml: CPB env vars for postgresql + n8n services - docker-compose.prod.yml: CPB env vars for n8n (prod overrides) - scripts/init-db.sh: append CPB user/database creation block - scripts/cpb-setup-db.sh: new idempotent production DB setup script Co-Authored-By: Claude Opus 4.6 --- .env.example | 36 ++++++++++++++++++++++++ docker-compose.prod.yml | 14 ++++++++++ docker-compose.yml | 17 ++++++++++++ scripts/cpb-setup-db.sh | 61 +++++++++++++++++++++++++++++++++++++++++ scripts/init-db.sh | 4 +++ 5 files changed, 132 insertions(+) create mode 100755 scripts/cpb-setup-db.sh diff --git a/.env.example b/.env.example index 4f8bdca7..7ee65788 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,24 @@ OPENSEARCH_PORT=9200 TEMPORAL_PORT=7233 TEMPORAL_UI_PORT=8080 +# CPB (Connecting People Bot) — Development +POSTGRES_DB_CPB=cpb_bot +POSTGRES_USER_CPB=cpb_app +POSTGRES_PASSWORD_CPB=cpb_password +CPB_POSTGRES_HOST=postgresql +CPB_POSTGRES_DB=cpb_bot +CPB_POSTGRES_USER=cpb_app +CPB_POSTGRES_PASSWORD=cpb_password +CPB_SLACK_BOT_TOKEN=xoxb-placeholder +CPB_CHANNEL_ID=CXXXXXXXXX +CPB_REPORT_CHANNEL_ID=CXXXXXXXXX +CPB_ADMIN_SLACK_ID=UXXXXXXXXX +CPB_DEV_SLACK_ID=UXXXXXXXXX +CPB_PAIRING_LAMBDA=0.0578 +CPB_PAIRING_ALPHA=0.3 +CPB_PAIRING_TRIALS=50 +CPB_PAIRING_MIN_WEIGHT=0.01 + # OAuth2 Proxy / Google OAuth GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret @@ -95,3 +113,21 @@ MN_SERVICE_SA_GOOGLE_TOKEN_URI=https://oauth2.googleapis.com/token MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL=https://www.googleapis.com/robot/v1/metadata/x509/mock-service-account%40mock-project-id.iam.gserviceaccount.com MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN=googleapis.com + +# CPB (Connecting People Bot) — Production +POSTGRES_DB_CPB=cpb_bot +POSTGRES_USER_CPB=cpb_app +POSTGRES_PASSWORD_CPB= +CPB_POSTGRES_HOST= +CPB_POSTGRES_DB=cpb_bot +CPB_POSTGRES_USER=cpb_app +CPB_POSTGRES_PASSWORD= +CPB_SLACK_BOT_TOKEN= +CPB_CHANNEL_ID=FILL_BEFORE_LAUNCH +CPB_REPORT_CHANNEL_ID=FILL_BEFORE_LAUNCH +CPB_ADMIN_SLACK_ID=FILL_BEFORE_LAUNCH +CPB_DEV_SLACK_ID=FILL_BEFORE_LAUNCH +CPB_PAIRING_LAMBDA=0.0578 +CPB_PAIRING_ALPHA=0.3 +CPB_PAIRING_TRIALS=50 +CPB_PAIRING_MIN_WEIGHT=0.01 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b492a7e1..37710467 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -36,6 +36,20 @@ services: - MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL=${MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL:?MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL is required} - MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL=${MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL:?MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL is required} - MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN=${MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN:?MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN is required} + # CPB (Connecting People Bot) + - CPB_POSTGRES_HOST=${POSTGRES_HOST:?POSTGRES_HOST is required} + - CPB_POSTGRES_DB=${POSTGRES_DB_CPB:?POSTGRES_DB_CPB is required} + - CPB_POSTGRES_USER=${POSTGRES_USER_CPB:?POSTGRES_USER_CPB is required} + - CPB_POSTGRES_PASSWORD=${POSTGRES_PASSWORD_CPB:?POSTGRES_PASSWORD_CPB is required} + - CPB_SLACK_BOT_TOKEN=${CPB_SLACK_BOT_TOKEN:?CPB_SLACK_BOT_TOKEN is required} + - CPB_CHANNEL_ID=${CPB_CHANNEL_ID:-FILL_BEFORE_LAUNCH} + - CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:-FILL_BEFORE_LAUNCH} + - CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:-FILL_BEFORE_LAUNCH} + - CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:-FILL_BEFORE_LAUNCH} + - CPB_PAIRING_LAMBDA=${CPB_PAIRING_LAMBDA:-0.0578} + - CPB_PAIRING_ALPHA=${CPB_PAIRING_ALPHA:-0.3} + - CPB_PAIRING_TRIALS=${CPB_PAIRING_TRIALS:-50} + - CPB_PAIRING_MIN_WEIGHT=${CPB_PAIRING_MIN_WEIGHT:-0.01} volumes: - n8n_data:/data/n8n postgresql: !reset null diff --git a/docker-compose.yml b/docker-compose.yml index a6efd8d8..9d5ca9ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,20 @@ services: - N8N_LOG_LEVEL=debug - N8N_LOG_OUTPUT=console - TZ=${TZ:-America/New_York} + # CPB (Connecting People Bot) + - CPB_POSTGRES_HOST=postgresql + - CPB_POSTGRES_DB=${POSTGRES_DB_CPB:-cpb_bot} + - CPB_POSTGRES_USER=${POSTGRES_USER_CPB:-cpb_app} + - CPB_POSTGRES_PASSWORD=${POSTGRES_PASSWORD_CPB:-cpb_password} + - CPB_SLACK_BOT_TOKEN=${CPB_SLACK_BOT_TOKEN:-xoxb-placeholder} + - CPB_CHANNEL_ID=${CPB_CHANNEL_ID:-CXXXXXXXXX} + - CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:-CXXXXXXXXX} + - CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:-UXXXXXXXXX} + - CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:-UXXXXXXXXX} + - CPB_PAIRING_LAMBDA=${CPB_PAIRING_LAMBDA:-0.0578} + - CPB_PAIRING_ALPHA=${CPB_PAIRING_ALPHA:-0.3} + - CPB_PAIRING_TRIALS=${CPB_PAIRING_TRIALS:-50} + - CPB_PAIRING_MIN_WEIGHT=${CPB_PAIRING_MIN_WEIGHT:-0.01} - MN_SERVICE_SA_GOOGLE_TYPE=${MN_SERVICE_SA_GOOGLE_TYPE:-GOOGLE_TYPE} - MN_SERVICE_SA_GOOGLE_PROJECT_ID=${MN_SERVICE_SA_GOOGLE_PROJECT_ID:-PROJECT_ID} - MN_SERVICE_SA_GOOGLE_PRIVATE_KEY_ID=${MN_SERVICE_SA_GOOGLE_PRIVATE_KEY_ID:-PRIVATE_KEY_ID} @@ -69,6 +83,9 @@ services: POSTGRES_USER_TEMPORAL: ${POSTGRES_USER_TEMPORAL:-temporal} POSTGRES_PASSWORD_TEMPORAL: ${POSTGRES_PASSWORD_TEMPORAL:-temporal} POSTGRES_DB_TEMPORAL_VISIBILITY: ${POSTGRES_DB_TEMPORAL_VISIBILITY:-temporal_visibility} + POSTGRES_DB_CPB: ${POSTGRES_DB_CPB:-cpb_bot} + POSTGRES_USER_CPB: ${POSTGRES_USER_CPB:-cpb_app} + POSTGRES_PASSWORD_CPB: ${POSTGRES_PASSWORD_CPB:-cpb_password} ports: - ${POSTGRES_PORT:-5432}:5432 volumes: diff --git a/scripts/cpb-setup-db.sh b/scripts/cpb-setup-db.sh new file mode 100755 index 00000000..27f12e71 --- /dev/null +++ b/scripts/cpb-setup-db.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +# ============================================================================= +# CPB Production Database Setup +# ============================================================================= +# Creates the cpb_bot database and cpb_app user on an external PostgreSQL server. +# Idempotent — safe to re-run. Does not modify existing data. +# +# Usage: +# POSTGRES_PASSWORD_CPB="" CPB_POSTGRES_HOST="" ./scripts/cpb-setup-db.sh +# +# Required env vars: +# CPB_POSTGRES_HOST — PostgreSQL server hostname or IP +# POSTGRES_PASSWORD_CPB — Password for the cpb_app user +# +# Optional env vars: +# CPB_POSTGRES_PORT — PostgreSQL port (default: 5432) +# POSTGRES_DB_CPB — Database name (default: cpb_bot) +# POSTGRES_USER_CPB — Username (default: cpb_app) +# ============================================================================= + +PGHOST="${CPB_POSTGRES_HOST:?CPB_POSTGRES_HOST is required}" +PGPORT="${CPB_POSTGRES_PORT:-5432}" +CPB_DB="${POSTGRES_DB_CPB:-cpb_bot}" +CPB_USER="${POSTGRES_USER_CPB:-cpb_app}" +CPB_PASS="${POSTGRES_PASSWORD_CPB:?POSTGRES_PASSWORD_CPB is required}" + +# Escape single quotes for SQL safety (prevents SQL injection via password) +CPB_PASS_SQL="${CPB_PASS//\'/\'\'}" + +echo "Setting up CPB database on ${PGHOST}:${PGPORT}..." +echo " Database: ${CPB_DB}" +echo " User: ${CPB_USER}" + +psql -h "$PGHOST" -p "$PGPORT" -U postgres <<-EOSQL +-- Create role if it doesn't exist +DO \$\$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${CPB_USER}') THEN + CREATE ROLE "${CPB_USER}" WITH LOGIN ENCRYPTED PASSWORD '${CPB_PASS_SQL}'; + RAISE NOTICE 'Created role: ${CPB_USER}'; + ELSE + RAISE NOTICE 'Role already exists: ${CPB_USER}'; + END IF; +END +\$\$; + +-- Create database if it doesn't exist +SELECT 'CREATE DATABASE "${CPB_DB}" OWNER "${CPB_USER}"' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${CPB_DB}')\gexec + +-- Grant privileges (idempotent) +GRANT ALL PRIVILEGES ON DATABASE "${CPB_DB}" TO "${CPB_USER}"; +EOSQL + +echo "" +echo "CPB database setup complete." +echo " Database: ${CPB_DB}" +echo " User: ${CPB_USER}" +echo " Host: ${PGHOST}:${PGPORT}" diff --git a/scripts/init-db.sh b/scripts/init-db.sh index ab9f0910..3fed7322 100755 --- a/scripts/init-db.sh +++ b/scripts/init-db.sh @@ -13,4 +13,8 @@ psql -v ON_ERROR_STOP=1 --username "postgres" <<-EOSQL ALTER USER "$POSTGRES_USER_TEMPORAL" CREATEDB; CREATE DATABASE "$POSTGRES_DB_TEMPORAL_VISIBILITY" OWNER "$POSTGRES_USER_TEMPORAL"; GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_TEMPORAL_VISIBILITY" TO "$POSTGRES_USER_TEMPORAL"; + + CREATE USER "$POSTGRES_USER_CPB" WITH ENCRYPTED PASSWORD '$POSTGRES_PASSWORD_CPB'; + CREATE DATABASE "$POSTGRES_DB_CPB" OWNER "$POSTGRES_USER_CPB"; + GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_CPB" TO "$POSTGRES_USER_CPB"; EOSQL From 8c673c364f1dbe643c290f73320ff17a3e05f83b Mon Sep 17 00:00:00 2001 From: SashkoMarchuk Date: Thu, 2 Apr 2026 21:57:54 +0700 Subject: [PATCH 2/6] fix(cpb): harden cpb-setup-db.sh against SQL injection and silent failures - Add identifier validation for username/database name (regex + 63-char limit) - Add -v ON_ERROR_STOP=1 to psql invocation to catch SQL errors - Add pipefail to catch pipeline failures - Fix misleading comment on password escaping Co-Authored-By: Claude Opus 4.6 --- scripts/cpb-setup-db.sh | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/cpb-setup-db.sh b/scripts/cpb-setup-db.sh index 27f12e71..b33ab983 100755 --- a/scripts/cpb-setup-db.sh +++ b/scripts/cpb-setup-db.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -eo pipefail # ============================================================================= # CPB Production Database Setup @@ -26,14 +26,30 @@ CPB_DB="${POSTGRES_DB_CPB:-cpb_bot}" CPB_USER="${POSTGRES_USER_CPB:-cpb_app}" CPB_PASS="${POSTGRES_PASSWORD_CPB:?POSTGRES_PASSWORD_CPB is required}" -# Escape single quotes for SQL safety (prevents SQL injection via password) +# Validate PostgreSQL identifiers (prevent SQL injection via crafted names) +validate_pg_identifier() { + local value="$1" name="$2" + if [[ -z "$value" ]]; then + echo "ERROR: ${name} cannot be empty" >&2; exit 1 + fi + if [[ ${#value} -gt 63 ]]; then + echo "ERROR: ${name} exceeds PostgreSQL's 63-char identifier limit" >&2; exit 1 + fi + if [[ ! "$value" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + echo "ERROR: ${name} contains invalid characters (must match ^[a-zA-Z_][a-zA-Z0-9_]*$)" >&2; exit 1 + fi +} +validate_pg_identifier "$CPB_USER" "POSTGRES_USER_CPB" +validate_pg_identifier "$CPB_DB" "POSTGRES_DB_CPB" + +# Escape single quotes in password for SQL string literal safety CPB_PASS_SQL="${CPB_PASS//\'/\'\'}" echo "Setting up CPB database on ${PGHOST}:${PGPORT}..." echo " Database: ${CPB_DB}" echo " User: ${CPB_USER}" -psql -h "$PGHOST" -p "$PGPORT" -U postgres <<-EOSQL +psql -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U postgres <<-EOSQL -- Create role if it doesn't exist DO \$\$ BEGIN From e74a8b753aa569ce7b049f94c1aac60e4a14daf8 Mon Sep 17 00:00:00 2001 From: SashkoMarchuk Date: Thu, 2 Apr 2026 22:39:30 +0700 Subject: [PATCH 3/6] fix(cpb): address CodeRabbit review findings for PR #120 - Remove duplicate CPB env vars from production section in .env.example - Make Slack IDs required (:?) in docker-compose.prod.yml instead of defaulting to placeholders - Make PostgreSQL admin user configurable via CPB_POSTGRES_ADMIN_USER in cpb-setup-db.sh - Fix password escaping in init-db.sh and cpb-setup-db.sh (use '' not \'\') Co-Authored-By: Claude Opus 4.6 --- .env.example | 23 +++++------------------ docker-compose.prod.yml | 8 ++++---- scripts/cpb-setup-db.sh | 12 +++++++----- scripts/init-db.sh | 5 ++++- 4 files changed, 20 insertions(+), 28 deletions(-) diff --git a/.env.example b/.env.example index 7ee65788..31d3c97b 100644 --- a/.env.example +++ b/.env.example @@ -113,21 +113,8 @@ MN_SERVICE_SA_GOOGLE_TOKEN_URI=https://oauth2.googleapis.com/token MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL=https://www.googleapis.com/robot/v1/metadata/x509/mock-service-account%40mock-project-id.iam.gserviceaccount.com MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN=googleapis.com - -# CPB (Connecting People Bot) — Production -POSTGRES_DB_CPB=cpb_bot -POSTGRES_USER_CPB=cpb_app -POSTGRES_PASSWORD_CPB= -CPB_POSTGRES_HOST= -CPB_POSTGRES_DB=cpb_bot -CPB_POSTGRES_USER=cpb_app -CPB_POSTGRES_PASSWORD= -CPB_SLACK_BOT_TOKEN= -CPB_CHANNEL_ID=FILL_BEFORE_LAUNCH -CPB_REPORT_CHANNEL_ID=FILL_BEFORE_LAUNCH -CPB_ADMIN_SLACK_ID=FILL_BEFORE_LAUNCH -CPB_DEV_SLACK_ID=FILL_BEFORE_LAUNCH -CPB_PAIRING_LAMBDA=0.0578 -CPB_PAIRING_ALPHA=0.3 -CPB_PAIRING_TRIALS=50 -CPB_PAIRING_MIN_WEIGHT=0.01 +# CPB (Connecting People Bot) — Production overrides +# Variables are defined in the Development section above (lines 30-46). +# For production: set real values for POSTGRES_PASSWORD_CPB, CPB_POSTGRES_HOST, +# CPB_SLACK_BOT_TOKEN, CPB_CHANNEL_ID, CPB_REPORT_CHANNEL_ID, +# CPB_ADMIN_SLACK_ID, and CPB_DEV_SLACK_ID in your .env file. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 37710467..e5316e90 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -42,10 +42,10 @@ services: - CPB_POSTGRES_USER=${POSTGRES_USER_CPB:?POSTGRES_USER_CPB is required} - CPB_POSTGRES_PASSWORD=${POSTGRES_PASSWORD_CPB:?POSTGRES_PASSWORD_CPB is required} - CPB_SLACK_BOT_TOKEN=${CPB_SLACK_BOT_TOKEN:?CPB_SLACK_BOT_TOKEN is required} - - CPB_CHANNEL_ID=${CPB_CHANNEL_ID:-FILL_BEFORE_LAUNCH} - - CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:-FILL_BEFORE_LAUNCH} - - CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:-FILL_BEFORE_LAUNCH} - - CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:-FILL_BEFORE_LAUNCH} + - CPB_CHANNEL_ID=${CPB_CHANNEL_ID:?CPB_CHANNEL_ID is required} + - CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:?CPB_REPORT_CHANNEL_ID is required} + - CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:?CPB_ADMIN_SLACK_ID is required} + - CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:?CPB_DEV_SLACK_ID is required} - CPB_PAIRING_LAMBDA=${CPB_PAIRING_LAMBDA:-0.0578} - CPB_PAIRING_ALPHA=${CPB_PAIRING_ALPHA:-0.3} - CPB_PAIRING_TRIALS=${CPB_PAIRING_TRIALS:-50} diff --git a/scripts/cpb-setup-db.sh b/scripts/cpb-setup-db.sh index b33ab983..41110dd4 100755 --- a/scripts/cpb-setup-db.sh +++ b/scripts/cpb-setup-db.sh @@ -15,13 +15,15 @@ set -eo pipefail # POSTGRES_PASSWORD_CPB — Password for the cpb_app user # # Optional env vars: -# CPB_POSTGRES_PORT — PostgreSQL port (default: 5432) -# POSTGRES_DB_CPB — Database name (default: cpb_bot) -# POSTGRES_USER_CPB — Username (default: cpb_app) +# CPB_POSTGRES_PORT — PostgreSQL port (default: 5432) +# CPB_POSTGRES_ADMIN_USER — Admin user for psql connection (default: postgres) +# POSTGRES_DB_CPB — Database name (default: cpb_bot) +# POSTGRES_USER_CPB — Username (default: cpb_app) # ============================================================================= PGHOST="${CPB_POSTGRES_HOST:?CPB_POSTGRES_HOST is required}" PGPORT="${CPB_POSTGRES_PORT:-5432}" +PGADMIN_USER="${CPB_POSTGRES_ADMIN_USER:-postgres}" CPB_DB="${POSTGRES_DB_CPB:-cpb_bot}" CPB_USER="${POSTGRES_USER_CPB:-cpb_app}" CPB_PASS="${POSTGRES_PASSWORD_CPB:?POSTGRES_PASSWORD_CPB is required}" @@ -43,13 +45,13 @@ validate_pg_identifier "$CPB_USER" "POSTGRES_USER_CPB" validate_pg_identifier "$CPB_DB" "POSTGRES_DB_CPB" # Escape single quotes in password for SQL string literal safety -CPB_PASS_SQL="${CPB_PASS//\'/\'\'}" +CPB_PASS_SQL="${CPB_PASS//\'/''}" echo "Setting up CPB database on ${PGHOST}:${PGPORT}..." echo " Database: ${CPB_DB}" echo " User: ${CPB_USER}" -psql -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U postgres <<-EOSQL +psql -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGADMIN_USER" <<-EOSQL -- Create role if it doesn't exist DO \$\$ BEGIN diff --git a/scripts/init-db.sh b/scripts/init-db.sh index 3fed7322..df50ab4d 100755 --- a/scripts/init-db.sh +++ b/scripts/init-db.sh @@ -1,6 +1,9 @@ #!/bin/bash set -e +# Escape single quotes in CPB password for SQL safety +ESCAPED_POSTGRES_PASSWORD_CPB="${POSTGRES_PASSWORD_CPB//\'/''}" + psql -v ON_ERROR_STOP=1 --username "postgres" <<-EOSQL CREATE USER "$POSTGRES_USER_N8N" WITH ENCRYPTED PASSWORD '$POSTGRES_PASSWORD_N8N'; CREATE DATABASE "$POSTGRES_DB_N8N" OWNER "$POSTGRES_USER_N8N"; @@ -14,7 +17,7 @@ psql -v ON_ERROR_STOP=1 --username "postgres" <<-EOSQL CREATE DATABASE "$POSTGRES_DB_TEMPORAL_VISIBILITY" OWNER "$POSTGRES_USER_TEMPORAL"; GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_TEMPORAL_VISIBILITY" TO "$POSTGRES_USER_TEMPORAL"; - CREATE USER "$POSTGRES_USER_CPB" WITH ENCRYPTED PASSWORD '$POSTGRES_PASSWORD_CPB'; + CREATE USER "$POSTGRES_USER_CPB" WITH ENCRYPTED PASSWORD '$ESCAPED_POSTGRES_PASSWORD_CPB'; CREATE DATABASE "$POSTGRES_DB_CPB" OWNER "$POSTGRES_USER_CPB"; GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_CPB" TO "$POSTGRES_USER_CPB"; EOSQL From 4c3ddedd9afdf0b1ecc65e09f982a921b5adf5e9 Mon Sep 17 00:00:00 2001 From: SashkoMarchuk Date: Thu, 2 Apr 2026 23:11:44 +0700 Subject: [PATCH 4/6] fix(cpb): address remaining nitpick findings and SonarCloud gate - Add clarifying comments in .env.example for provisioning vs connection vars - Escape N8N and Temporal passwords in init-db.sh for consistency with CPB fix - Add explicit return 0 to validate_pg_identifier (fixes SonarCloud S7682) Co-Authored-By: Claude Opus 4.6 --- .env.example | 2 ++ scripts/cpb-setup-db.sh | 1 + scripts/init-db.sh | 8 +++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 31d3c97b..710a8683 100644 --- a/.env.example +++ b/.env.example @@ -28,9 +28,11 @@ TEMPORAL_PORT=7233 TEMPORAL_UI_PORT=8080 # CPB (Connecting People Bot) — Development +## PostgreSQL provisioning (used by init-db.sh to create the database and role) POSTGRES_DB_CPB=cpb_bot POSTGRES_USER_CPB=cpb_app POSTGRES_PASSWORD_CPB=cpb_password +## CPB application connection (passed to the app at runtime) CPB_POSTGRES_HOST=postgresql CPB_POSTGRES_DB=cpb_bot CPB_POSTGRES_USER=cpb_app diff --git a/scripts/cpb-setup-db.sh b/scripts/cpb-setup-db.sh index 41110dd4..08afd564 100755 --- a/scripts/cpb-setup-db.sh +++ b/scripts/cpb-setup-db.sh @@ -40,6 +40,7 @@ validate_pg_identifier() { if [[ ! "$value" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then echo "ERROR: ${name} contains invalid characters (must match ^[a-zA-Z_][a-zA-Z0-9_]*$)" >&2; exit 1 fi + return 0 } validate_pg_identifier "$CPB_USER" "POSTGRES_USER_CPB" validate_pg_identifier "$CPB_DB" "POSTGRES_DB_CPB" diff --git a/scripts/init-db.sh b/scripts/init-db.sh index df50ab4d..a8689299 100755 --- a/scripts/init-db.sh +++ b/scripts/init-db.sh @@ -1,15 +1,17 @@ #!/bin/bash set -e -# Escape single quotes in CPB password for SQL safety +# Escape single quotes in passwords for SQL safety +ESCAPED_POSTGRES_PASSWORD_N8N="${POSTGRES_PASSWORD_N8N//\'/''}" +ESCAPED_POSTGRES_PASSWORD_TEMPORAL="${POSTGRES_PASSWORD_TEMPORAL//\'/''}" ESCAPED_POSTGRES_PASSWORD_CPB="${POSTGRES_PASSWORD_CPB//\'/''}" psql -v ON_ERROR_STOP=1 --username "postgres" <<-EOSQL - CREATE USER "$POSTGRES_USER_N8N" WITH ENCRYPTED PASSWORD '$POSTGRES_PASSWORD_N8N'; + CREATE USER "$POSTGRES_USER_N8N" WITH ENCRYPTED PASSWORD '$ESCAPED_POSTGRES_PASSWORD_N8N'; CREATE DATABASE "$POSTGRES_DB_N8N" OWNER "$POSTGRES_USER_N8N"; GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_N8N" TO "$POSTGRES_USER_N8N"; - CREATE USER "$POSTGRES_USER_TEMPORAL" WITH ENCRYPTED PASSWORD '$POSTGRES_PASSWORD_TEMPORAL'; + CREATE USER "$POSTGRES_USER_TEMPORAL" WITH ENCRYPTED PASSWORD '$ESCAPED_POSTGRES_PASSWORD_TEMPORAL'; CREATE DATABASE "$POSTGRES_DB_TEMPORAL" OWNER "$POSTGRES_USER_TEMPORAL"; GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_TEMPORAL" TO "$POSTGRES_USER_TEMPORAL"; From 47d422169d8a9375111e27f96e4a66979ea40f52 Mon Sep 17 00:00:00 2001 From: SashkoMarchuk Date: Thu, 2 Apr 2026 23:17:42 +0700 Subject: [PATCH 5/6] fix(cpb): add identifier validation to init-db.sh and fix production note - Add validate_pg_identifier to init-db.sh matching cpb-setup-db.sh pattern - Validate all 7 database/user identifiers before SQL execution - Fix .env.example production note: correct line range (30-48), add CPB_POSTGRES_PASSWORD Co-Authored-By: Claude Opus 4.6 --- .env.example | 6 +++--- scripts/init-db.sh | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 710a8683..b8fedb21 100644 --- a/.env.example +++ b/.env.example @@ -116,7 +116,7 @@ MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oaut MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL=https://www.googleapis.com/robot/v1/metadata/x509/mock-service-account%40mock-project-id.iam.gserviceaccount.com MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN=googleapis.com # CPB (Connecting People Bot) — Production overrides -# Variables are defined in the Development section above (lines 30-46). +# Variables are defined in the Development section above (lines 30-48). # For production: set real values for POSTGRES_PASSWORD_CPB, CPB_POSTGRES_HOST, -# CPB_SLACK_BOT_TOKEN, CPB_CHANNEL_ID, CPB_REPORT_CHANNEL_ID, -# CPB_ADMIN_SLACK_ID, and CPB_DEV_SLACK_ID in your .env file. +# CPB_POSTGRES_PASSWORD, CPB_SLACK_BOT_TOKEN, CPB_CHANNEL_ID, +# CPB_REPORT_CHANNEL_ID, CPB_ADMIN_SLACK_ID, and CPB_DEV_SLACK_ID in your .env file. diff --git a/scripts/init-db.sh b/scripts/init-db.sh index a8689299..5171788e 100755 --- a/scripts/init-db.sh +++ b/scripts/init-db.sh @@ -1,6 +1,29 @@ #!/bin/bash set -e +# Validate PostgreSQL identifiers (prevent SQL injection via crafted names) +validate_pg_identifier() { + local value="$1" name="$2" + if [[ -z "$value" ]]; then + echo "ERROR: ${name} cannot be empty" >&2; exit 1 + fi + if [[ ${#value} -gt 63 ]]; then + echo "ERROR: ${name} exceeds PostgreSQL's 63-char identifier limit" >&2; exit 1 + fi + if [[ ! "$value" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + echo "ERROR: ${name} contains invalid characters (must match ^[a-zA-Z_][a-zA-Z0-9_]*$)" >&2; exit 1 + fi + return 0 +} + +validate_pg_identifier "$POSTGRES_USER_N8N" "POSTGRES_USER_N8N" +validate_pg_identifier "$POSTGRES_DB_N8N" "POSTGRES_DB_N8N" +validate_pg_identifier "$POSTGRES_USER_TEMPORAL" "POSTGRES_USER_TEMPORAL" +validate_pg_identifier "$POSTGRES_DB_TEMPORAL" "POSTGRES_DB_TEMPORAL" +validate_pg_identifier "$POSTGRES_DB_TEMPORAL_VISIBILITY" "POSTGRES_DB_TEMPORAL_VISIBILITY" +validate_pg_identifier "$POSTGRES_USER_CPB" "POSTGRES_USER_CPB" +validate_pg_identifier "$POSTGRES_DB_CPB" "POSTGRES_DB_CPB" + # Escape single quotes in passwords for SQL safety ESCAPED_POSTGRES_PASSWORD_N8N="${POSTGRES_PASSWORD_N8N//\'/''}" ESCAPED_POSTGRES_PASSWORD_TEMPORAL="${POSTGRES_PASSWORD_TEMPORAL//\'/''}" From d2ed4f851b1048cb34fa48c1f13852cb005770db Mon Sep 17 00:00:00 2001 From: SashkoMarchuk Date: Sat, 4 Apr 2026 01:29:31 +0700 Subject: [PATCH 6/6] fix(cpb): use safe defaults for CPB env vars in production compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch 9 CPB environment variables from :?required to :-default in docker-compose.prod.yml. Docker Compose evaluates all :?required vars globally during file parsing — missing CPB vars would crash ALL services (n8n, temporal, redis, oauth2-proxy), not just CPB functionality. With :-default, deploys work safely without CPB vars configured. CPB won't function (placeholder token, fake IDs) but nothing crashes. Co-Authored-By: Claude Opus 4.6 --- docker-compose.prod.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e5316e90..0434be0a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -36,16 +36,16 @@ services: - MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL=${MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL:?MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL is required} - MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL=${MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL:?MN_SERVICE_SA_GOOGLE_CLIENT_X509_CERT_URL is required} - MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN=${MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN:?MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN is required} - # CPB (Connecting People Bot) - - CPB_POSTGRES_HOST=${POSTGRES_HOST:?POSTGRES_HOST is required} - - CPB_POSTGRES_DB=${POSTGRES_DB_CPB:?POSTGRES_DB_CPB is required} - - CPB_POSTGRES_USER=${POSTGRES_USER_CPB:?POSTGRES_USER_CPB is required} - - CPB_POSTGRES_PASSWORD=${POSTGRES_PASSWORD_CPB:?POSTGRES_PASSWORD_CPB is required} - - CPB_SLACK_BOT_TOKEN=${CPB_SLACK_BOT_TOKEN:?CPB_SLACK_BOT_TOKEN is required} - - CPB_CHANNEL_ID=${CPB_CHANNEL_ID:?CPB_CHANNEL_ID is required} - - CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:?CPB_REPORT_CHANNEL_ID is required} - - CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:?CPB_ADMIN_SLACK_ID is required} - - CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:?CPB_DEV_SLACK_ID is required} + # CPB (Connecting People Bot) — defaults allow safe deploy before CPB is configured + - CPB_POSTGRES_HOST=${POSTGRES_HOST:-localhost} + - CPB_POSTGRES_DB=${POSTGRES_DB_CPB:-cpb_bot} + - CPB_POSTGRES_USER=${POSTGRES_USER_CPB:-cpb_app} + - CPB_POSTGRES_PASSWORD=${POSTGRES_PASSWORD_CPB:-cpb_password} + - CPB_SLACK_BOT_TOKEN=${CPB_SLACK_BOT_TOKEN:-xoxb-placeholder} + - CPB_CHANNEL_ID=${CPB_CHANNEL_ID:-CXXXXXXXXX} + - CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:-CXXXXXXXXX} + - CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:-UXXXXXXXXX} + - CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:-UXXXXXXXXX} - CPB_PAIRING_LAMBDA=${CPB_PAIRING_LAMBDA:-0.0578} - CPB_PAIRING_ALPHA=${CPB_PAIRING_ALPHA:-0.3} - CPB_PAIRING_TRIALS=${CPB_PAIRING_TRIALS:-50}