From 935d3442b8aa7402fc6f2a5f715eb509a104c986 Mon Sep 17 00:00:00 2001 From: Jeff Maki Date: Fri, 10 Jul 2026 16:40:42 -0400 Subject: [PATCH] Change stub user to validate OSM rails models --- ...f3a7b9c1d2e4_heal_short_tdei_pass_crypt.py | 47 +++++++++++++++++++ api/core/security.py | 8 +++- api/src/tasking/projects/repository.py | 8 +++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 alembic_osm/versions/f3a7b9c1d2e4_heal_short_tdei_pass_crypt.py diff --git a/alembic_osm/versions/f3a7b9c1d2e4_heal_short_tdei_pass_crypt.py b/alembic_osm/versions/f3a7b9c1d2e4_heal_short_tdei_pass_crypt.py new file mode 100644 index 0000000..5ff6bca --- /dev/null +++ b/alembic_osm/versions/f3a7b9c1d2e4_heal_short_tdei_pass_crypt.py @@ -0,0 +1,47 @@ +"""heal short TDEI-provisioned users.pass_crypt + +TDEI users are provisioned by the backend via raw SQL with a placeholder +``pass_crypt``. Older rows used a 4-char value (``"none"``), which violates OSM's +``pass_crypt`` length 8..255 ``User`` validation. That makes the user invalid, +and Rails operations that re-validate the author via ``validates :author, +:associated => true`` (posting a changeset comment or a note comment) then fail +to save. Replace any too-short value with a random throwaway -- TDEI manages +auth, so ``pass_crypt`` is never used to authenticate. + +Revision ID: f3a7b9c1d2e4 +Revises: 5303f61d7b3a +Create Date: 2026-07-11 00:00:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +from sqlalchemy import inspect, text + +# revision identifiers, used by Alembic. +revision: str = "f3a7b9c1d2e4" +down_revision: Union[str, None] = "5303f61d7b3a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + # `users` is owned by the OSM Rails website, not this tree; guard so the + # migration is a no-op if it hasn't been created yet. + if not inspect(bind).has_table("users"): + return + + # Idempotent: re-running only touches rows that are still too short. + op.execute( + text( + "UPDATE users SET pass_crypt = md5(random()::text) " + "WHERE auth_provider = 'TDEI' AND length(pass_crypt) < 8" + ) + ) + + +def downgrade() -> None: + # The original short values are unrecoverable (and were invalid anyway). + pass diff --git a/api/core/security.py b/api/core/security.py index adeaa38..020abf8 100644 --- a/api/core/security.py +++ b/api/core/security.py @@ -354,7 +354,7 @@ async def _ensure_osm_user( "INSERT INTO users (auth_uid, email, display_name, auth_provider, " "status, pass_crypt, data_public, email_valid, terms_seen, " "creation_time, terms_agreed, tou_agreed) " - "VALUES (:auth_uid, :email, :name, 'TDEI', 'active', 'none', " + "VALUES (:auth_uid, :email, :name, 'TDEI', 'active', :pass_crypt, " "true, true, true, (now() at time zone 'utc'), " "(now() at time zone 'utc'), (now() at time zone 'utc')) " "ON CONFLICT (auth_uid) DO NOTHING" @@ -363,6 +363,12 @@ async def _ensure_osm_user( "auth_uid": auth_uid, "email": email or f"{auth_uid}@tdei.invalid", "name": display_name, + # OSM's User model validates pass_crypt length 8..255. TDEI manages + # auth, so this is a throwaway that just satisfies that rule (a too- + # short value makes the user invalid and breaks Rails operations + # that re-validate it via `validates :author, :associated => true`, + # e.g. posting a changeset comment). + "pass_crypt": secrets.token_hex(16), }, ) row = ( diff --git a/api/src/tasking/projects/repository.py b/api/src/tasking/projects/repository.py index 3aa6820..44bbc42 100644 --- a/api/src/tasking/projects/repository.py +++ b/api/src/tasking/projects/repository.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import secrets from datetime import datetime from typing import Any from uuid import UUID @@ -313,13 +314,18 @@ async def _provision_users_from_tdei( await self.session.execute( text( "INSERT INTO users (auth_uid, email, display_name, auth_provider, status, pass_crypt, data_public, email_valid, terms_seen, creation_time, terms_agreed, tou_agreed) " - "VALUES (:uid, :email, :name, 'TDEI', 'active', 'none', true, true, true, (now() at time zone 'utc'), (now() at time zone 'utc'), (now() at time zone 'utc')) " + "VALUES (:uid, :email, :name, 'TDEI', 'active', :pass_crypt, true, true, true, (now() at time zone 'utc'), (now() at time zone 'utc'), (now() at time zone 'utc')) " "ON CONFLICT (auth_uid) DO NOTHING" ), params={ "uid": uid, "email": member.email, "name": member.display_name, + # OSM validates pass_crypt length 8..255; a too-short value + # makes the user invalid and breaks Rails ops that re-validate + # the author (changeset/note comments). TDEI manages auth, so + # this is a throwaway. + "pass_crypt": secrets.token_hex(16), }, ) resolved.add(uid)