fix: double-encoded user.oauth in the b10670c03dd5 migration

Instances that had not upgraded since v0.6.41 and then jumped to v0.9.6 or later lose sign-in for every OAuth user, with a 500 and a validation error on the user model. Password sign-in for the same accounts fails identically, because every read path validates the row.

The migration that backfills user.oauth from the legacy oauth_sub column was rewritten from raw SQL to Core DML in May and kept its json.dumps call. The column reference declares sa.JSON, so SQLAlchemy serializes the value a second time and the row ends up holding a JSON string instead of an object. Reading it back yields a str, which the dict-typed field rejects. The mirrored json.loads in downgrade() has the inverse problem and silently nulled oauth_sub for every user.

Binding the dict directly fixes the write. Correcting that migration cannot help a database that already crossed it, since alembic never re-runs a stamped revision, so a follow-up revision un-double-encodes affected rows in place. It is a no-op for rows that were never corrupted.

Fixes #28101
This commit is contained in:
Classic298 2026-08-04 17:32:54 +02:00
parent 8dbbc206c5
commit 6c274623da
2 changed files with 37 additions and 7 deletions

View file

@ -185,9 +185,8 @@ def upgrade() -> None:
for uid, oauth_sub in rows:
if oauth_sub:
provider, sub = oauth_sub.split('@', 1) if '@' in oauth_sub else ('oidc', oauth_sub)
conn.execute(
sa.update(_user).where(_user.c.id == uid).values(oauth=json.dumps({provider: {'sub': sub}}))
)
# no json.dumps here: the column is JSON, SQLAlchemy serializes it
conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth={provider: {'sub': sub}}))
# ── Migrate api_key column → api_key table (only if old column still exists)
if 'api_key' in user_columns:
@ -226,10 +225,8 @@ def downgrade() -> None:
for uid, oauth in rows:
try:
data = json.loads(oauth)
provider = list(data.keys())[0]
sub = data[provider].get('sub')
oauth_sub = f'{provider}@{sub}'
provider = next(iter(oauth))
oauth_sub = f'{provider}@{oauth[provider]["sub"]}'
except Exception:
oauth_sub = None

View file

@ -0,0 +1,33 @@
"""Repair double-encoded user.oauth values
Revision ID: d4a71b6e29c8
Revises: 1ce6ade7d93b
Create Date: 2026-08-04 12:00:00.000000
"""
import json
import sqlalchemy as sa
from alembic import op
revision = 'd4a71b6e29c8'
down_revision = '1ce6ade7d93b'
branch_labels = None
depends_on = None
_user = sa.table('user', sa.column('id', sa.Text), sa.column('oauth', sa.JSON))
def upgrade():
conn = op.get_bind()
rows = conn.execute(sa.select(_user.c.id, _user.c.oauth).where(_user.c.oauth.is_not(None))).fetchall()
for uid, oauth in rows:
# b10670c03dd5 used to serialize this column twice, leaving a JSON string instead of an object
if isinstance(oauth, str):
conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth=json.loads(oauth)))
def downgrade():
pass