Files
Project-SDE-WP-Suite/server/alembic/versions/b7e4f1a20c93_drop_user_password_hash.py
Cody Schaefer 8cfb4c1008 T10.3 D13 - drop users.password_hash and every path that touched it
The irreversible one. The suite no longer stores a credential of any kind.

Removed from app.py: /api/auth/forgot-password, /api/auth/reset-password,
/api/auth/reset-available, /api/auth/password, /api/auth/users/{id}/password,
the four password-bearing input models, the reset throttle and mail body, and
the password arguments to create_user. Removed from auth.py: hash_password,
verify_password, password_problem, MIN_PASSWORD_LEN, _COMMON_PASSWORDS,
create_reset_token, decode_reset_token, RESET_MINUTES, and the bcrypt import.
Removed from notify.py: the password_reset_enabled feature flag. Removed from
manage_users.py: the password prompt and the reset-password command.

token_version STAYS. Password changes no longer exist, but a role change or a
deactivation still has to invalidate sessions that are already issued.

/api/auth/users/{id}/role stays, which is D13 criterion 4 - granting admin to
an existing account must keep working, and it does.

Migration b7e4f1a20c93 uses batch_alter_table because SQLite has no DROP COLUMN
before 3.35 and local dev runs on SQLite while production runs on Postgres.
downgrade() recreates the column NULLABLE rather than NOT NULL as the baseline
declared it: there are no hashes to put back, and a NOT NULL column with no
server default refuses to add itself to a table with rows. The docstring says
plainly that the downgrade does not restore the old login - it exists so the
revision is well-formed, not because stepping back is a recovery path.

Verified:

  upgrade head from empty      -> password_hash absent from users
  downgrade -1                 -> column back, nullable (notnull=0)
  upgrade head again           -> absent again
  remaining /api/auth routes   -> no password or reset route left
  create-admin                 -> works with no password prompt
  grep for the removed symbols -> nothing outside the migration and one
                                  docstring that names the dropped column

NOT verified, and it is a done-when box left open rather than ticked: the
migration has only been round-tripped on SQLite. No Postgres is available here.
batch_alter_table takes the direct ALTER path on Postgres, which is the simpler
of the two, but "simpler" is not "tested".

notify.send_now is now orphaned - its only caller was forgot_password. Logged as
BL-026 rather than deleted in passing, because an immediate unqueued send is a
reasonable primitive to keep and that decision does not belong in an auth task.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:26:43 -05:00

43 lines
1.6 KiB
Python

"""drop users.password_hash — D13 / T10.3
Authentication moved to an LDAPS simple bind against the domain (see
server/ldap_auth.py), so the suite no longer holds a credential of any kind.
THIS MIGRATION DESTROYS DATA AND CANNOT BE UNDONE IN ANY MEANINGFUL SENSE.
`downgrade()` recreates the column, but every hash in it is gone — and even a
restored hash would be useless, because nothing reads the column any more. The
downgrade exists so the revision is well-formed and so an operator can step back
past it, not because stepping back restores the old login. To actually revert to
local passwords you have to revert the application code and reset every password
by hand.
The column is recreated NULLABLE on downgrade, deliberately. The baseline schema
declared it NOT NULL, but there are no values to put back, so a NOT NULL column
with no server default would refuse to add itself on any table that has rows.
Revision ID: b7e4f1a20c93
Revises: a1b8c6d4e2f9
Create Date: 2026-08-21
"""
import sqlalchemy as sa
from alembic import op
revision = 'b7e4f1a20c93'
down_revision = 'a1b8c6d4e2f9'
branch_labels = None
depends_on = None
def upgrade():
# batch_alter_table for SQLite's benefit: it has no DROP COLUMN before 3.35,
# so alembic rebuilds the table. Postgres drops it directly. Local dev runs on
# SQLite and production on Postgres, so this has to work on both.
with op.batch_alter_table('users') as batch:
batch.drop_column('password_hash')
def downgrade():
with op.batch_alter_table('users') as batch:
batch.add_column(sa.Column('password_hash', sa.String(length=200),
nullable=True))