|
| 1 | +#!/bin/bash |
| 2 | +set -eo pipefail |
| 3 | + |
| 4 | +# ============================================================================= |
| 5 | +# CPB Production Database Setup |
| 6 | +# ============================================================================= |
| 7 | +# Creates the cpb_bot database on an external PostgreSQL (RDS) server using the |
| 8 | +# temporal user (which has CREATEDB privilege). Grants access to the n8n user |
| 9 | +# so the CPB application (running inside n8n) can manage its tables. |
| 10 | +# |
| 11 | +# Ownership model: |
| 12 | +# - temporal creates and owns the database (only user with CREATEDB) |
| 13 | +# - n8n gets ALL PRIVILEGES on the database to create/manage tables |
| 14 | +# - Later, when DevOps provides the master password, ownership can be |
| 15 | +# migrated to a dedicated cpb_app user |
| 16 | +# |
| 17 | +# PostgreSQL version notes: |
| 18 | +# - PG14: public schema grants CREATE to PUBLIC by default — n8n can create |
| 19 | +# tables without explicit schema grants |
| 20 | +# - PG15+: CREATE on public schema is revoked by default — the script |
| 21 | +# attempts to grant schema privileges and warns if it cannot (the DB owner |
| 22 | +# can do this on PG15+ since public schema is owned by pg_database_owner) |
| 23 | +# |
| 24 | +# Usage: |
| 25 | +# CPB_POSTGRES_HOST="<host>" POSTGRES_PASSWORD_TEMPORAL="<pw>" \ |
| 26 | +# POSTGRES_USER_N8N="n8n" ./scripts/cpb-setup-db.sh |
| 27 | +# |
| 28 | +# Required env vars: |
| 29 | +# CPB_POSTGRES_HOST — PostgreSQL server hostname or IP |
| 30 | +# POSTGRES_PASSWORD_TEMPORAL — Password for the temporal user |
| 31 | +# |
| 32 | +# Optional env vars: |
| 33 | +# CPB_POSTGRES_PORT — PostgreSQL port (default: 5432) |
| 34 | +# POSTGRES_USER_TEMPORAL — Temporal user name (default: temporal) |
| 35 | +# POSTGRES_USER_N8N — n8n user to grant access to (default: n8n) |
| 36 | +# POSTGRES_DB_CPB — Database name (default: cpb_bot) |
| 37 | +# ============================================================================= |
| 38 | + |
| 39 | +PGHOST="${CPB_POSTGRES_HOST:?CPB_POSTGRES_HOST is required}" |
| 40 | +PGPORT="${CPB_POSTGRES_PORT:-5432}" |
| 41 | +TEMPORAL_USER="${POSTGRES_USER_TEMPORAL:-temporal}" |
| 42 | +N8N_USER="${POSTGRES_USER_N8N:-n8n}" |
| 43 | +CPB_DB="${POSTGRES_DB_CPB:-cpb_bot}" |
| 44 | + |
| 45 | +TEMPORAL_PASS="${POSTGRES_PASSWORD_TEMPORAL:?POSTGRES_PASSWORD_TEMPORAL is required}" |
| 46 | + |
| 47 | +# Validate PostgreSQL identifiers (prevent SQL injection via crafted names) |
| 48 | +validate_pg_identifier() { |
| 49 | + local value="$1" name="$2" |
| 50 | + if [[ -z "$value" ]]; then |
| 51 | + echo "ERROR: ${name} cannot be empty" >&2; exit 1 |
| 52 | + fi |
| 53 | + if [[ ${#value} -gt 63 ]]; then |
| 54 | + echo "ERROR: ${name} exceeds PostgreSQL's 63-char identifier limit" >&2; exit 1 |
| 55 | + fi |
| 56 | + if [[ ! "$value" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then |
| 57 | + echo "ERROR: ${name} contains invalid characters (must match ^[a-zA-Z_][a-zA-Z0-9_]*$)" >&2; exit 1 |
| 58 | + fi |
| 59 | + return 0 |
| 60 | +} |
| 61 | +validate_pg_identifier "$TEMPORAL_USER" "POSTGRES_USER_TEMPORAL" |
| 62 | +validate_pg_identifier "$N8N_USER" "POSTGRES_USER_N8N" |
| 63 | +validate_pg_identifier "$CPB_DB" "POSTGRES_DB_CPB" |
| 64 | + |
| 65 | +echo "Setting up CPB database on ${PGHOST}:${PGPORT}..." |
| 66 | +echo " Database: ${CPB_DB}" |
| 67 | +echo " Owner: ${TEMPORAL_USER} (interim — migrate to dedicated user later)" |
| 68 | +echo " Grantee: ${N8N_USER}" |
| 69 | + |
| 70 | +# --- Step 1: Create database and grant database-level privileges ----------- |
| 71 | +# Connect to the 'postgres' maintenance database to run DDL. |
| 72 | +# CREATE DATABASE cannot run inside a transaction, so we use \gexec. |
| 73 | +PGPASSWORD="${TEMPORAL_PASS}" psql -v ON_ERROR_STOP=1 \ |
| 74 | + -h "$PGHOST" -p "$PGPORT" -U "$TEMPORAL_USER" -d postgres <<-EOSQL |
| 75 | +-- Create database if it doesn't exist (temporal becomes owner) |
| 76 | +SELECT 'CREATE DATABASE "${CPB_DB}"' |
| 77 | +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${CPB_DB}')\gexec |
| 78 | +
|
| 79 | +-- Grant all database-level privileges to n8n (idempotent) |
| 80 | +GRANT ALL PRIVILEGES ON DATABASE "${CPB_DB}" TO "${N8N_USER}"; |
| 81 | +EOSQL |
| 82 | + |
| 83 | +# --- Step 2: Grant schema-level privileges --------------------------------- |
| 84 | +# On PG14, this is unnecessary (PUBLIC has CREATE on public schema by default) |
| 85 | +# but we attempt it for forward-compatibility with PG15+ where it IS required. |
| 86 | +# temporal cannot grant on public schema in PG14 (owned by postgres), so we |
| 87 | +# handle the error gracefully. |
| 88 | +SCHEMA_GRANT_ERR=$(PGPASSWORD="${TEMPORAL_PASS}" psql -v ON_ERROR_STOP=1 \ |
| 89 | + -h "$PGHOST" -p "$PGPORT" -U "$TEMPORAL_USER" -d "$CPB_DB" \ |
| 90 | + -c "GRANT ALL ON SCHEMA public TO \"${N8N_USER}\";" 2>&1) && \ |
| 91 | + echo " Schema grant on public: OK" || { |
| 92 | + if echo "$SCHEMA_GRANT_ERR" | grep -qi "permission denied\|must be owner"; then |
| 93 | + echo " Schema grant on public: skipped (not needed on PG14 — PUBLIC has CREATE by default)" |
| 94 | + echo " NOTE: After upgrading to PG15+, re-run this script or grant manually:" |
| 95 | + echo " GRANT ALL ON SCHEMA public TO \"${N8N_USER}\";" |
| 96 | + else |
| 97 | + echo "ERROR: Schema grant failed unexpectedly:" >&2 |
| 98 | + echo " $SCHEMA_GRANT_ERR" >&2 |
| 99 | + exit 1 |
| 100 | + fi |
| 101 | +} |
| 102 | + |
| 103 | +echo "" |
| 104 | +echo "CPB database setup complete." |
| 105 | +echo " Database: ${CPB_DB}" |
| 106 | +echo " Owner: ${TEMPORAL_USER}" |
| 107 | +echo " Access: ${N8N_USER} (all privileges)" |
| 108 | +echo " Host: ${PGHOST}:${PGPORT}" |
0 commit comments