diff --git a/docker-compose.yml b/docker-compose.yml index 90a387f..f1f255a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,9 @@ x-app-environment: &app-environment DATABASE_HOST: db DATABASE_NAME: dtf DATABASE_USER: dtf_app - DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + # Must differ from POSTGRES_PASSWORD: the runtime role is DML-only, and reusing + # the administrator credential would make that restriction meaningless. + DATABASE_PASSWORD: ${APP_DB_PASSWORD:?set APP_DB_PASSWORD} S3_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} S3_BUCKET: ${R2_BUCKET:?set R2_BUCKET} @@ -62,7 +64,7 @@ services: DATABASE_ADMIN_USER: dtf_admin DATABASE_ADMIN_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} APP_DB_USER: dtf_app - APP_DB_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + APP_DB_PASSWORD: ${APP_DB_PASSWORD:?set APP_DB_PASSWORD} networks: [backend] deploy: replicas: 1 diff --git a/local/bootstrap.py b/local/bootstrap.py index 30fcf8e..71de430 100644 --- a/local/bootstrap.py +++ b/local/bootstrap.py @@ -1,6 +1,7 @@ """One-shot schema/role setup. Only this job receives database admin credentials.""" import os from pathlib import Path +from urllib.parse import urlparse import psycopg from psycopg import sql @@ -16,8 +17,18 @@ def admin_connect(): return psycopg.connect(os.environ['DATABASE_ADMIN_URL']) +def admin_password(): + if os.environ.get('DATABASE_ADMIN_HOST'): + return os.environ.get('DATABASE_ADMIN_PASSWORD') + url = os.environ.get('DATABASE_ADMIN_URL', '') + return urlparse(url).password + + def main(): role = os.environ['APP_DB_USER'] + password = os.environ['APP_DB_PASSWORD'] + if password == admin_password(): + raise RuntimeError('Application and database administrator passwords must differ') with admin_connect() as c: admin, database = c.execute('SELECT current_user,current_database()').fetchone() if role == admin: @@ -25,7 +36,7 @@ def main(): if not c.execute('SELECT 1 FROM pg_roles WHERE rolname=%s', (role,)).fetchone(): c.execute(sql.SQL('CREATE ROLE {} LOGIN').format(sql.Identifier(role))) c.execute(sql.SQL('ALTER ROLE {} NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS PASSWORD {}').format( - sql.Identifier(role), sql.Literal(os.environ['APP_DB_PASSWORD']))) + sql.Identifier(role), sql.Literal(password))) c.execute(Path(__file__).with_name('schema.sql').read_text()) c.execute(sql.SQL('REVOKE CREATE ON SCHEMA public FROM PUBLIC')) c.execute(sql.SQL('GRANT CONNECT ON DATABASE {} TO {}').format(sql.Identifier(database),sql.Identifier(role)))