Skip to content

Commit 09436df

Browse files
infra(cpb): add database, env vars, and setup scripts for Connecting People Bot (#120)
## Summary - Add CPB (Connecting People Bot) PostgreSQL database infrastructure: dedicated `cpb_bot` database, `cpb_app` user, and all required environment variables - Add `scripts/cpb-setup-db.sh` — idempotent production database setup script with SQL injection protection and error handling - Append CPB user/database creation to `scripts/init-db.sh` for dev environment auto-initialization All changes are **additive** — no existing n8n or Temporal functionality is modified. ## Files Changed | File | Change | |------|--------| | `.env.example` | CPB variables for dev (with defaults) and prod (with placeholders) sections | | `docker-compose.yml` | CPB env vars for `postgresql` service (3 vars) + `n8n` service (14 vars) | | `docker-compose.prod.yml` | CPB env vars for `n8n` service with `:?required` / `:-default` syntax | | `scripts/init-db.sh` | Append CPB user/database creation block (dev auto-init) | | `scripts/cpb-setup-db.sh` | **New file** — idempotent production database setup | ## Security Hardening The production setup script (`cpb-setup-db.sh`) includes: - **SQL injection prevention**: `validate_pg_identifier()` function validates username/database name against `^[a-zA-Z_][a-zA-Z0-9_]*$` with 63-char PostgreSQL limit - **Error detection**: `-v ON_ERROR_STOP=1` ensures psql exits on SQL errors (not silently ignored) - **Pipeline safety**: `set -eo pipefail` catches both command and pipeline failures - **Password escaping**: Single quotes escaped for SQL string literal safety ## Setup Guide for DevOps ### Development (fresh setup) ```bash # 1. Copy env vars (CPB defaults are already included) cp .env.example .env # Edit .env — set CPB_SLACK_BOT_TOKEN to existing SLACK_TOKEN value for dev testing # 2. Start services (fresh volume required for init-db.sh to run) docker compose down -v docker compose up -d # 3. Verify CPB database exists docker compose exec postgresql psql -U cpb_app -d cpb_bot -c "SELECT 1" # 4. Verify CPB env vars are visible to n8n docker compose exec n8n env | grep CPB_ ``` ### Development (existing volume) If you already have a running PostgreSQL volume, `init-db.sh` won't re-run. Create the CPB database manually: ```bash docker compose exec postgresql psql -U postgres <<-EOSQL CREATE USER cpb_app WITH ENCRYPTED PASSWORD 'cpb_password'; CREATE DATABASE cpb_bot OWNER cpb_app; GRANT ALL PRIVILEGES ON DATABASE cpb_bot TO cpb_app; EOSQL # Restart n8n to pick up new env vars docker compose restart n8n docker compose exec n8n env | grep CPB_ ``` ### Production ```bash # 1. Generate a secure password openssl rand -base64 32 # 2. Run the idempotent setup script POSTGRES_PASSWORD_CPB="<generated-password>" \ CPB_POSTGRES_HOST="<prod-postgresql-host>" \ ./scripts/cpb-setup-db.sh # 3. Update production .env with real values: # - POSTGRES_PASSWORD_CPB (the generated password) # - CPB_POSTGRES_HOST (already set via POSTGRES_HOST) # - CPB_SLACK_BOT_TOKEN (from Al — CPB-specific xoxb- token) # - CPB_CHANNEL_ID, CPB_REPORT_CHANNEL_ID (from Olivia/Al) # - CPB_ADMIN_SLACK_ID, CPB_DEV_SLACK_ID (Slack user IDs) # 4. Deploy docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d # 5. Verify docker compose exec n8n env | grep CPB_ ``` ## Environment Variables Reference | Variable | Dev Default | Prod | Description | |----------|-------------|------|-------------| | `POSTGRES_DB_CPB` | `cpb_bot` | `cpb_bot` | Database name | | `POSTGRES_USER_CPB` | `cpb_app` | `cpb_app` | Database user | | `POSTGRES_PASSWORD_CPB` | `cpb_password` | **generate** | Database password | | `CPB_SLACK_BOT_TOKEN` | `xoxb-placeholder` | **from Al** | Slack bot token | | `CPB_CHANNEL_ID` | `CXXXXXXXXX` | **from Olivia** | CPB community channel | | `CPB_REPORT_CHANNEL_ID` | `CXXXXXXXXX` | **from Al** | Admin report channel | | `CPB_ADMIN_SLACK_ID` | `UXXXXXXXXX` | **from Olivia** | Admin Slack user ID | | `CPB_DEV_SLACK_ID` | `UXXXXXXXXX` | **from Sashko** | Developer Slack user ID | | `CPB_PAIRING_LAMBDA` | `0.0578` | `0.0578` | Pairing decay rate | | `CPB_PAIRING_ALPHA` | `0.3` | `0.3` | Repeat penalty | | `CPB_PAIRING_TRIALS` | `50` | `50` | Matching trials | | `CPB_PAIRING_MIN_WEIGHT` | `0.01` | `0.01` | Minimum weight floor | ## Rollback All changes are additive. To rollback: ```bash # Dev: remove volumes and revert docker compose down -v git revert <this-commit> docker compose up -d # Prod: drop CPB database/user psql -h <prod-host> -U postgres -c "DROP DATABASE IF EXISTS cpb_bot; DROP ROLE IF EXISTS cpb_app;" ``` ## Test Plan - [ ] `docker compose config --quiet` passes (dev config valid) - [ ] `docker compose -f docker-compose.yml -f docker-compose.prod.yml config --quiet` validates (prod config — will error on missing required vars, expected) - [ ] `bash -n scripts/cpb-setup-db.sh` passes (no syntax errors) - [ ] `grep -c "CPB" scripts/init-db.sh` returns 3 (user, database, grant) - [ ] `test -x scripts/cpb-setup-db.sh` confirms executable - [ ] Fresh `docker compose up -d` (after `down -v`) creates `cpb_bot` database - [ ] `docker compose exec postgresql psql -U cpb_app -d cpb_bot -c "SELECT 1"` returns 1 - [ ] `docker compose exec n8n env | grep CPB_` lists all 14 CPB variables <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Connecting People Bot: Slack-based pairing with persistent storage and configurable pairing parameters (lambda, alpha, trials, min weight). * **Chores** * Added Development/Production environment configuration and automated DB provisioning/init support to simplify setup and deployment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8c250de commit 09436df

5 files changed

Lines changed: 170 additions & 2 deletions

File tree

.env.example

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ OPENSEARCH_PORT=9200
2727
TEMPORAL_PORT=7233
2828
TEMPORAL_UI_PORT=8080
2929

30+
# CPB (Connecting People Bot) — Development
31+
## PostgreSQL provisioning (used by init-db.sh to create the database and role)
32+
POSTGRES_DB_CPB=cpb_bot
33+
POSTGRES_USER_CPB=cpb_app
34+
POSTGRES_PASSWORD_CPB=cpb_password
35+
## CPB application connection (passed to the app at runtime)
36+
CPB_POSTGRES_HOST=postgresql
37+
CPB_POSTGRES_DB=cpb_bot
38+
CPB_POSTGRES_USER=cpb_app
39+
CPB_POSTGRES_PASSWORD=cpb_password
40+
CPB_SLACK_BOT_TOKEN=xoxb-placeholder
41+
CPB_CHANNEL_ID=CXXXXXXXXX
42+
CPB_REPORT_CHANNEL_ID=CXXXXXXXXX
43+
CPB_ADMIN_SLACK_ID=UXXXXXXXXX
44+
CPB_DEV_SLACK_ID=UXXXXXXXXX
45+
CPB_PAIRING_LAMBDA=0.0578
46+
CPB_PAIRING_ALPHA=0.3
47+
CPB_PAIRING_TRIALS=50
48+
CPB_PAIRING_MIN_WEIGHT=0.01
49+
3050
# OAuth2 Proxy / Google OAuth
3151
GOOGLE_CLIENT_ID=your_google_client_id
3252
GOOGLE_CLIENT_SECRET=your_google_client_secret
@@ -95,3 +115,8 @@ MN_SERVICE_SA_GOOGLE_TOKEN_URI=https://oauth2.googleapis.com/token
95115
MN_SERVICE_SA_GOOGLE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
96116
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
97117
MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN=googleapis.com
118+
# CPB (Connecting People Bot) — Production overrides
119+
# Variables are defined in the Development section above (lines 30-48).
120+
# For production: set real values for POSTGRES_PASSWORD_CPB, CPB_POSTGRES_HOST,
121+
# CPB_POSTGRES_PASSWORD, CPB_SLACK_BOT_TOKEN, CPB_CHANNEL_ID,
122+
# CPB_REPORT_CHANNEL_ID, CPB_ADMIN_SLACK_ID, and CPB_DEV_SLACK_ID in your .env file.

docker-compose.prod.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ services:
3636
- 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}
3737
- 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}
3838
- MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN=${MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN:?MN_SERVICE_SA_GOOGLE_UNIVERSE_DOMAIN is required}
39+
# CPB (Connecting People Bot) — defaults allow safe deploy before CPB is configured
40+
- CPB_POSTGRES_HOST=${POSTGRES_HOST:-localhost}
41+
- CPB_POSTGRES_DB=${POSTGRES_DB_CPB:-cpb_bot}
42+
- CPB_POSTGRES_USER=${POSTGRES_USER_CPB:-cpb_app}
43+
- CPB_POSTGRES_PASSWORD=${POSTGRES_PASSWORD_CPB:-cpb_password}
44+
- CPB_SLACK_BOT_TOKEN=${CPB_SLACK_BOT_TOKEN:-xoxb-placeholder}
45+
- CPB_CHANNEL_ID=${CPB_CHANNEL_ID:-CXXXXXXXXX}
46+
- CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:-CXXXXXXXXX}
47+
- CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:-UXXXXXXXXX}
48+
- CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:-UXXXXXXXXX}
49+
- CPB_PAIRING_LAMBDA=${CPB_PAIRING_LAMBDA:-0.0578}
50+
- CPB_PAIRING_ALPHA=${CPB_PAIRING_ALPHA:-0.3}
51+
- CPB_PAIRING_TRIALS=${CPB_PAIRING_TRIALS:-50}
52+
- CPB_PAIRING_MIN_WEIGHT=${CPB_PAIRING_MIN_WEIGHT:-0.01}
3953
volumes:
4054
- n8n_data:/data/n8n
4155
postgresql: !reset null

docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ services:
3434
- N8N_LOG_LEVEL=debug
3535
- N8N_LOG_OUTPUT=console
3636
- TZ=${TZ:-America/New_York}
37+
# CPB (Connecting People Bot)
38+
- CPB_POSTGRES_HOST=postgresql
39+
- CPB_POSTGRES_DB=${POSTGRES_DB_CPB:-cpb_bot}
40+
- CPB_POSTGRES_USER=${POSTGRES_USER_CPB:-cpb_app}
41+
- CPB_POSTGRES_PASSWORD=${POSTGRES_PASSWORD_CPB:-cpb_password}
42+
- CPB_SLACK_BOT_TOKEN=${CPB_SLACK_BOT_TOKEN:-xoxb-placeholder}
43+
- CPB_CHANNEL_ID=${CPB_CHANNEL_ID:-CXXXXXXXXX}
44+
- CPB_REPORT_CHANNEL_ID=${CPB_REPORT_CHANNEL_ID:-CXXXXXXXXX}
45+
- CPB_ADMIN_SLACK_ID=${CPB_ADMIN_SLACK_ID:-UXXXXXXXXX}
46+
- CPB_DEV_SLACK_ID=${CPB_DEV_SLACK_ID:-UXXXXXXXXX}
47+
- CPB_PAIRING_LAMBDA=${CPB_PAIRING_LAMBDA:-0.0578}
48+
- CPB_PAIRING_ALPHA=${CPB_PAIRING_ALPHA:-0.3}
49+
- CPB_PAIRING_TRIALS=${CPB_PAIRING_TRIALS:-50}
50+
- CPB_PAIRING_MIN_WEIGHT=${CPB_PAIRING_MIN_WEIGHT:-0.01}
3751
- MN_SERVICE_SA_GOOGLE_TYPE=${MN_SERVICE_SA_GOOGLE_TYPE:-GOOGLE_TYPE}
3852
- MN_SERVICE_SA_GOOGLE_PROJECT_ID=${MN_SERVICE_SA_GOOGLE_PROJECT_ID:-PROJECT_ID}
3953
- MN_SERVICE_SA_GOOGLE_PRIVATE_KEY_ID=${MN_SERVICE_SA_GOOGLE_PRIVATE_KEY_ID:-PRIVATE_KEY_ID}
@@ -69,6 +83,9 @@ services:
6983
POSTGRES_USER_TEMPORAL: ${POSTGRES_USER_TEMPORAL:-temporal}
7084
POSTGRES_PASSWORD_TEMPORAL: ${POSTGRES_PASSWORD_TEMPORAL:-temporal}
7185
POSTGRES_DB_TEMPORAL_VISIBILITY: ${POSTGRES_DB_TEMPORAL_VISIBILITY:-temporal_visibility}
86+
POSTGRES_DB_CPB: ${POSTGRES_DB_CPB:-cpb_bot}
87+
POSTGRES_USER_CPB: ${POSTGRES_USER_CPB:-cpb_app}
88+
POSTGRES_PASSWORD_CPB: ${POSTGRES_PASSWORD_CPB:-cpb_password}
7289
ports:
7390
- ${POSTGRES_PORT:-5432}:5432
7491
volumes:

scripts/cpb-setup-db.sh

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/bin/bash
2+
set -eo pipefail
3+
4+
# =============================================================================
5+
# CPB Production Database Setup
6+
# =============================================================================
7+
# Creates the cpb_bot database and cpb_app user on an external PostgreSQL server.
8+
# Idempotent — safe to re-run. Does not modify existing data.
9+
#
10+
# Usage:
11+
# POSTGRES_PASSWORD_CPB="<password>" CPB_POSTGRES_HOST="<host>" ./scripts/cpb-setup-db.sh
12+
#
13+
# Required env vars:
14+
# CPB_POSTGRES_HOST — PostgreSQL server hostname or IP
15+
# POSTGRES_PASSWORD_CPB — Password for the cpb_app user
16+
#
17+
# Optional env vars:
18+
# CPB_POSTGRES_PORT — PostgreSQL port (default: 5432)
19+
# CPB_POSTGRES_ADMIN_USER — Admin user for psql connection (default: postgres)
20+
# POSTGRES_DB_CPB — Database name (default: cpb_bot)
21+
# POSTGRES_USER_CPB — Username (default: cpb_app)
22+
# =============================================================================
23+
24+
PGHOST="${CPB_POSTGRES_HOST:?CPB_POSTGRES_HOST is required}"
25+
PGPORT="${CPB_POSTGRES_PORT:-5432}"
26+
PGADMIN_USER="${CPB_POSTGRES_ADMIN_USER:-postgres}"
27+
CPB_DB="${POSTGRES_DB_CPB:-cpb_bot}"
28+
CPB_USER="${POSTGRES_USER_CPB:-cpb_app}"
29+
CPB_PASS="${POSTGRES_PASSWORD_CPB:?POSTGRES_PASSWORD_CPB is required}"
30+
31+
# Validate PostgreSQL identifiers (prevent SQL injection via crafted names)
32+
validate_pg_identifier() {
33+
local value="$1" name="$2"
34+
if [[ -z "$value" ]]; then
35+
echo "ERROR: ${name} cannot be empty" >&2; exit 1
36+
fi
37+
if [[ ${#value} -gt 63 ]]; then
38+
echo "ERROR: ${name} exceeds PostgreSQL's 63-char identifier limit" >&2; exit 1
39+
fi
40+
if [[ ! "$value" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
41+
echo "ERROR: ${name} contains invalid characters (must match ^[a-zA-Z_][a-zA-Z0-9_]*$)" >&2; exit 1
42+
fi
43+
return 0
44+
}
45+
validate_pg_identifier "$CPB_USER" "POSTGRES_USER_CPB"
46+
validate_pg_identifier "$CPB_DB" "POSTGRES_DB_CPB"
47+
48+
# Escape single quotes in password for SQL string literal safety
49+
CPB_PASS_SQL="${CPB_PASS//\'/''}"
50+
51+
echo "Setting up CPB database on ${PGHOST}:${PGPORT}..."
52+
echo " Database: ${CPB_DB}"
53+
echo " User: ${CPB_USER}"
54+
55+
psql -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGADMIN_USER" <<-EOSQL
56+
-- Create role if it doesn't exist
57+
DO \$\$
58+
BEGIN
59+
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${CPB_USER}') THEN
60+
CREATE ROLE "${CPB_USER}" WITH LOGIN ENCRYPTED PASSWORD '${CPB_PASS_SQL}';
61+
RAISE NOTICE 'Created role: ${CPB_USER}';
62+
ELSE
63+
RAISE NOTICE 'Role already exists: ${CPB_USER}';
64+
END IF;
65+
END
66+
\$\$;
67+
68+
-- Create database if it doesn't exist
69+
SELECT 'CREATE DATABASE "${CPB_DB}" OWNER "${CPB_USER}"'
70+
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${CPB_DB}')\gexec
71+
72+
-- Grant privileges (idempotent)
73+
GRANT ALL PRIVILEGES ON DATABASE "${CPB_DB}" TO "${CPB_USER}";
74+
EOSQL
75+
76+
echo ""
77+
echo "CPB database setup complete."
78+
echo " Database: ${CPB_DB}"
79+
echo " User: ${CPB_USER}"
80+
echo " Host: ${PGHOST}:${PGPORT}"

scripts/init-db.sh

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,48 @@
11
#!/bin/bash
22
set -e
33

4+
# Validate PostgreSQL identifiers (prevent SQL injection via crafted names)
5+
validate_pg_identifier() {
6+
local value="$1" name="$2"
7+
if [[ -z "$value" ]]; then
8+
echo "ERROR: ${name} cannot be empty" >&2; exit 1
9+
fi
10+
if [[ ${#value} -gt 63 ]]; then
11+
echo "ERROR: ${name} exceeds PostgreSQL's 63-char identifier limit" >&2; exit 1
12+
fi
13+
if [[ ! "$value" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
14+
echo "ERROR: ${name} contains invalid characters (must match ^[a-zA-Z_][a-zA-Z0-9_]*$)" >&2; exit 1
15+
fi
16+
return 0
17+
}
18+
19+
validate_pg_identifier "$POSTGRES_USER_N8N" "POSTGRES_USER_N8N"
20+
validate_pg_identifier "$POSTGRES_DB_N8N" "POSTGRES_DB_N8N"
21+
validate_pg_identifier "$POSTGRES_USER_TEMPORAL" "POSTGRES_USER_TEMPORAL"
22+
validate_pg_identifier "$POSTGRES_DB_TEMPORAL" "POSTGRES_DB_TEMPORAL"
23+
validate_pg_identifier "$POSTGRES_DB_TEMPORAL_VISIBILITY" "POSTGRES_DB_TEMPORAL_VISIBILITY"
24+
validate_pg_identifier "$POSTGRES_USER_CPB" "POSTGRES_USER_CPB"
25+
validate_pg_identifier "$POSTGRES_DB_CPB" "POSTGRES_DB_CPB"
26+
27+
# Escape single quotes in passwords for SQL safety
28+
ESCAPED_POSTGRES_PASSWORD_N8N="${POSTGRES_PASSWORD_N8N//\'/''}"
29+
ESCAPED_POSTGRES_PASSWORD_TEMPORAL="${POSTGRES_PASSWORD_TEMPORAL//\'/''}"
30+
ESCAPED_POSTGRES_PASSWORD_CPB="${POSTGRES_PASSWORD_CPB//\'/''}"
31+
432
psql -v ON_ERROR_STOP=1 --username "postgres" <<-EOSQL
5-
CREATE USER "$POSTGRES_USER_N8N" WITH ENCRYPTED PASSWORD '$POSTGRES_PASSWORD_N8N';
33+
CREATE USER "$POSTGRES_USER_N8N" WITH ENCRYPTED PASSWORD '$ESCAPED_POSTGRES_PASSWORD_N8N';
634
CREATE DATABASE "$POSTGRES_DB_N8N" OWNER "$POSTGRES_USER_N8N";
735
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_N8N" TO "$POSTGRES_USER_N8N";
836
9-
CREATE USER "$POSTGRES_USER_TEMPORAL" WITH ENCRYPTED PASSWORD '$POSTGRES_PASSWORD_TEMPORAL';
37+
CREATE USER "$POSTGRES_USER_TEMPORAL" WITH ENCRYPTED PASSWORD '$ESCAPED_POSTGRES_PASSWORD_TEMPORAL';
1038
CREATE DATABASE "$POSTGRES_DB_TEMPORAL" OWNER "$POSTGRES_USER_TEMPORAL";
1139
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_TEMPORAL" TO "$POSTGRES_USER_TEMPORAL";
1240
1341
ALTER USER "$POSTGRES_USER_TEMPORAL" CREATEDB;
1442
CREATE DATABASE "$POSTGRES_DB_TEMPORAL_VISIBILITY" OWNER "$POSTGRES_USER_TEMPORAL";
1543
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_TEMPORAL_VISIBILITY" TO "$POSTGRES_USER_TEMPORAL";
44+
45+
CREATE USER "$POSTGRES_USER_CPB" WITH ENCRYPTED PASSWORD '$ESCAPED_POSTGRES_PASSWORD_CPB';
46+
CREATE DATABASE "$POSTGRES_DB_CPB" OWNER "$POSTGRES_USER_CPB";
47+
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB_CPB" TO "$POSTGRES_USER_CPB";
1648
EOSQL

0 commit comments

Comments
 (0)