diff --git a/.env.example b/.env.example index 4f8bdca..b8fedb2 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,26 @@ OPENSEARCH_PORT=9200 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 +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 +115,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 overrides +# Variables are defined in the Development section above (lines 30-48). +# For production: set real values for POSTGRES_PASSWORD_CPB, CPB_POSTGRES_HOST, +# 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/docker-compose.prod.yml b/docker-compose.prod.yml index b492a7e..0434be0 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) — 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} + - 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 a6efd8d..9d5ca9e 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 0000000..08afd56 --- /dev/null +++ b/scripts/cpb-setup-db.sh @@ -0,0 +1,80 @@ +#!/bin/bash +set -eo pipefail + +# ============================================================================= +# 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) +# 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}" + +# 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 "$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 -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGADMIN_USER" <<-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 ab9f091..5171788 100755 --- a/scripts/init-db.sh +++ b/scripts/init-db.sh @@ -1,16 +1,48 @@ #!/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//\'/''}" +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"; 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 '$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