feat: migration — create resolutionflow_app and resolutionflow_admin DB roles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
102
backend/alembic/versions/0b470d9e6cf1_create_db_roles.py
Normal file
102
backend/alembic/versions/0b470d9e6cf1_create_db_roles.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"""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")
|
||||||
Reference in New Issue
Block a user