52 lines
1.7 KiB
Python
52 lines
1.7 KiB
Python
"""control0006_fernet_key_version
|
|
|
|
Adds the Fernet key-version stamp to the `tenants` table:
|
|
- key_version INT NOT NULL DEFAULT 1
|
|
|
|
MT-23: credentials are encrypted under a single control-plane Fernet key, and
|
|
before this there was no way to tell which rows had been re-encrypted after a
|
|
key change — rotation was all-or-nothing and unverifiable. Stamping the version
|
|
each time a row is written makes rotation progress a plain query:
|
|
|
|
SELECT COUNT(*) FROM tenants WHERE key_version < <current>;
|
|
|
|
Existing rows default to 1, which matches the single key in use before this
|
|
migration, so nothing needs re-encrypting to adopt it.
|
|
|
|
Guarded by an INFORMATION_SCHEMA existence check, so re-running is safe.
|
|
|
|
Revision ID: control0006_fernet_key_ver
|
|
Revises: control0005_superadmin_mfa
|
|
Create Date: 2026-08-21
|
|
"""
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
# NOTE: revision ids must fit VARCHAR(32) — 'control0006_fernet_key_ver' is 26.
|
|
revision = 'control0006_fernet_key_ver'
|
|
down_revision = 'control0005_superadmin_mfa'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def _column_exists(table, column):
|
|
result = op.get_bind().execute(sa.text(
|
|
"SELECT COUNT(*) FROM information_schema.COLUMNS "
|
|
"WHERE TABLE_SCHEMA = DATABASE() "
|
|
" AND TABLE_NAME = :tbl "
|
|
" AND COLUMN_NAME = :col"
|
|
), {'tbl': table, 'col': column})
|
|
return result.scalar() > 0
|
|
|
|
|
|
def upgrade():
|
|
if not _column_exists('tenants', 'key_version'):
|
|
op.add_column('tenants', sa.Column('key_version', sa.Integer(),
|
|
nullable=False, server_default='1'))
|
|
|
|
|
|
def downgrade():
|
|
if _column_exists('tenants', 'key_version'):
|
|
op.drop_column('tenants', 'key_version')
|