Files
saas_backend/alembic/versions/b2e1d4f5a602_encrypt_module_trust_credentials.py
T
2026-08-31 20:04:12 -04:00

91 lines
2.5 KiB
Python

"""Encrypt module trust credentials at rest.
`module_environments.trust_credentials` was a plain JSON column holding the
`hmac_secret` / `secret_key` values every module integration is authenticated
with. This adds an encrypted column, moves each row's secrets into it, and blanks
the plaintext.
Requires ENCRYPTION_KEY to be set, and it must be the same value the application
runs with — otherwise the application cannot read back what this wrote.
Revision ID: b2e1d4f5a602
Revises: a1f0c2d3e401
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import table, column
revision: str = "b2e1d4f5a602"
down_revision: Union[str, Sequence[str], None] = "a1f0c2d3e401"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
from app.core.crypto import encrypt_json
op.add_column(
"module_environments",
sa.Column("trust_credentials_enc", sa.Text(), nullable=True),
)
envs = table(
"module_environments",
column("id", sa.String),
column("trust_credentials", sa.JSON),
column("trust_credentials_enc", sa.Text),
)
conn = op.get_bind()
rows = conn.execute(
sa.text("SELECT id, trust_credentials FROM module_environments")
).fetchall()
migrated = 0
for row_id, creds in rows:
if not creds:
continue
if isinstance(creds, str):
import json
creds = json.loads(creds)
conn.execute(
sa.text(
"UPDATE module_environments "
"SET trust_credentials_enc = :enc, trust_credentials = '{}'::json "
"WHERE id = :id"
),
{"enc": encrypt_json(creds), "id": row_id},
)
migrated += 1
print(f" encrypted trust credentials for {migrated} module environment(s)")
def downgrade() -> None:
from app.core.crypto import decrypt_json
conn = op.get_bind()
rows = conn.execute(
sa.text(
"SELECT id, trust_credentials_enc FROM module_environments "
"WHERE trust_credentials_enc IS NOT NULL"
)
).fetchall()
import json
for row_id, enc in rows:
conn.execute(
sa.text(
"UPDATE module_environments SET trust_credentials = CAST(:c AS json) "
"WHERE id = :id"
),
{"c": json.dumps(decrypt_json(enc)), "id": row_id},
)
op.drop_column("module_environments", "trust_credentials_enc")