Files
ss-tools/backend/alembic/versions/a1b2c3d4e5f7_session_activity_logical_sessions.py

90 lines
4.0 KiB
Python

# #region Alembic.Migration.SessionActivityLogicalSessions [C:2] [TYPE Migration] [SEMANTICS alembic,migration,session,activity,sid]
# @BRIEF Convert session_activity from per-JWT tracking to logical-session (sid) tracking.
# @PRE Previous migration (o1p2q3r4s5t6) has been applied.
# @POST session_activity rows are keyed by sid; is_revoked column present for whole-session revocation.
# @SIDE_EFFECT DDL execution — renames jti -> sid, adds is_revoked column.
# @RELATION DEPENDS_ON -> [Models.Auth.SessionActivity]
# @RATIONALE Replacement JWTs share one logical session (sid). Existing rows carry their
# historical jti forward as sid; they are non-renewable but still enforceable.
# @REJECTED Dropping and recreating the table was rejected — loses existing audit rows and
# would break live deployments that already enforce idle timeouts.
"""convert session_activity to logical session rows
Revision ID: a1b2c3d4e5f7
Revises: o1p2q3r4s5t6
Create Date: 2026-08-06 12:00:00.000000
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect
# revision identifiers, used by Alembic.
revision: str = "a1b2c3d4e5f7"
down_revision: str | Sequence[str] | None = "o1p2q3r4s5t6"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
# #region Migration.SessionActivityLogicalSessions.TableExists [C:1] [TYPE Function] [SEMANTICS alembic,helper,table]
# @BRIEF Check if a table already exists in the database.
def _table_exists(table_name: str) -> bool:
conn = op.get_bind()
inspector = inspect(conn)
return table_name in inspector.get_table_names()
# #endregion Migration.SessionActivityLogicalSessions.TableExists
# #region Migration.SessionActivityLogicalSessions.ColumnNames [C:1] [TYPE Function] [SEMANTICS alembic,helper,column]
# @BRIEF Return the set of column names of a table.
def _column_names(table_name: str) -> set[str]:
inspector = inspect(op.get_bind())
return {c["name"] for c in inspector.get_columns(table_name)}
# #endregion Migration.SessionActivityLogicalSessions.ColumnNames
# #region Migration.SessionActivityLogicalSessions.Upgrade [C:2] [TYPE Function] [SEMANTICS alembic,upgrade]
# @BRIEF Rename jti -> sid (when needed) and add is_revoked column.
# @PRE session_activity table exists.
# @POST session_activity keyed by sid with is_revoked defaulting to False.
# @SIDE_EFFECT Executes ALTER TABLE DDL.
def upgrade() -> None:
"""Convert session_activity to logical-session keying and add revocation state."""
if not _table_exists("session_activity"):
return
cols = _column_names("session_activity")
with op.batch_alter_table("session_activity") as batch_op:
if "jti" in cols and "sid" not in cols:
batch_op.alter_column("jti", new_column_name="sid")
if "is_revoked" not in cols:
batch_op.add_column(
sa.Column(
"is_revoked",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
)
)
# #endregion Migration.SessionActivityLogicalSessions.Upgrade
# #region Migration.SessionActivityLogicalSessions.Downgrade [C:1] [TYPE Function] [SEMANTICS alembic,downgrade]
# @BRIEF Revert sid -> jti and drop is_revoked.
# @POST session_activity restored to per-JWT keying without revocation state.
# @SIDE_EFFECT Executes ALTER TABLE DDL.
def downgrade() -> None:
"""Revert logical-session keying back to jti and drop is_revoked."""
if not _table_exists("session_activity"):
return
cols = _column_names("session_activity")
with op.batch_alter_table("session_activity") as batch_op:
if "sid" in cols and "jti" not in cols:
batch_op.alter_column("sid", new_column_name="jti")
if "is_revoked" in cols:
batch_op.drop_column("is_revoked")
# #endregion Migration.SessionActivityLogicalSessions.Downgrade
# #endregion Alembic.Migration.SessionActivityLogicalSessions