"""create_db_roles Revision ID: 0b470d9e6cf1 Revises: a9f3b2c1d4e5 Create Date: 2026-04-10 03:58:10.207919 """ import os from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy import text # revision identifiers, used by Alembic. revision: str = '0b470d9e6cf1' down_revision: Union[str, None] = 'a9f3b2c1d4e5' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # Passwords from env vars. For local dev, defaults are sufficient. # For production (Railway), set DB_APP_ROLE_PASSWORD and # DB_ADMIN_ROLE_PASSWORD as environment variables before running migrations. # Passwords must not contain single quotes. app_pw = os.environ.get("DB_APP_ROLE_PASSWORD", "app_secret_change_me") admin_pw = os.environ.get("DB_ADMIN_ROLE_PASSWORD", "admin_secret_change_me") # Fetch the current database name dynamically — avoids hardcoding # (the DB is named 'resolutionflow' in dev, potentially different elsewhere). conn = op.get_bind() db_name = conn.execute(text("SELECT current_database()")).scalar() # ── Application role ──────────────────────────────────────────────────── # Subject to RLS. Used by FastAPI at runtime via DATABASE_URL. op.execute(f""" DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'resolutionflow_app') THEN CREATE ROLE resolutionflow_app LOGIN PASSWORD '{app_pw}'; ELSE ALTER ROLE resolutionflow_app LOGIN PASSWORD '{app_pw}'; END IF; END $$ """) op.execute(f"GRANT CONNECT ON DATABASE {db_name} TO resolutionflow_app") op.execute("GRANT USAGE ON SCHEMA public TO resolutionflow_app") op.execute( "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public " "TO resolutionflow_app" ) op.execute( "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO resolutionflow_app" ) # Ensure future tables automatically get the same permissions op.execute( "ALTER DEFAULT PRIVILEGES IN SCHEMA public " "GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO resolutionflow_app" ) op.execute( "ALTER DEFAULT PRIVILEGES IN SCHEMA public " "GRANT USAGE, SELECT ON SEQUENCES TO resolutionflow_app" ) # ── Admin role ────────────────────────────────────────────────────────── # BYPASSRLS. Used by Alembic (DATABASE_URL_SYNC) and /admin/* endpoints # (ADMIN_DATABASE_URL) after Task 11. op.execute(f""" DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'resolutionflow_admin') THEN CREATE ROLE resolutionflow_admin LOGIN PASSWORD '{admin_pw}'; ELSE ALTER ROLE resolutionflow_admin LOGIN PASSWORD '{admin_pw}'; END IF; END $$ """) op.execute("GRANT resolutionflow_app TO resolutionflow_admin") op.execute("ALTER ROLE resolutionflow_admin BYPASSRLS") op.execute(f"GRANT CONNECT ON DATABASE {db_name} TO resolutionflow_admin") def downgrade() -> None: conn = op.get_bind() db_name = conn.execute(text("SELECT current_database()")).scalar() op.execute( "REVOKE ALL ON ALL TABLES IN SCHEMA public FROM resolutionflow_app" ) op.execute( "REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM resolutionflow_app" ) op.execute( f"REVOKE CONNECT ON DATABASE {db_name} FROM resolutionflow_app" ) op.execute( f"REVOKE CONNECT ON DATABASE {db_name} FROM resolutionflow_admin" ) op.execute("DROP ROLE IF EXISTS resolutionflow_admin") op.execute("DROP ROLE IF EXISTS resolutionflow_app")