fix(user): 回填本地账号 external_id,修复 admin 密码登录失败 (#1985)
commit 94323e3ec 把登录查询从 user_name 切到 external_id,但未对已有本地 账号做数据迁移——源于
admin 及所有旧本地用户的 external_id=NULL,密码
登录一律返回 10600 "Account or password error"。
- Alembic f012: 为 source='local' 且 external_id IS NULL 的活跃用户回填
external_id = user_name。uk_user_source_external_id(source,external_id)
复合唯一键天然继承原 user_name 唯一性语义。
- UserService.create_user: 新建本地账号时自动把 external_id 填成 user_name, 堵住 UI
新建用户后无法登录的漏洞(SSO 路径走 org_sync,不经此函数)。
This commit is contained in:
+69
@@ -0,0 +1,69 @@
|
||||
"""F012: Backfill user.external_id for local accounts.
|
||||
|
||||
Revision ID: f012_backfill_local_external_id
|
||||
Revises: f011_backfill_create_knowledge_web_menu
|
||||
Create Date: 2026-04-19
|
||||
|
||||
Background:
|
||||
Commit 94323e3ec (2026-04-17) changed the password-login query from
|
||||
``User.user_name == account`` to ``User.external_id == account`` — aligned
|
||||
with F009's org-sync model where external_id is the stable identity — but
|
||||
did not ship a data migration for pre-existing local accounts. As a result,
|
||||
every row with ``source='local' AND external_id IS NULL`` (including the
|
||||
bootstrap admin) becomes unable to log in via password.
|
||||
|
||||
This migration backfills ``external_id = user_name`` for local accounts,
|
||||
restoring password login. The ``uk_user_source_external_id`` unique
|
||||
constraint on ``(source, external_id)`` naturally inherits user_name's
|
||||
former uniqueness within ``source='local'``.
|
||||
|
||||
Scope:
|
||||
- Only ``source='local'`` and ``delete=0`` rows are touched.
|
||||
- Rows that already have a non-NULL external_id are left alone (idempotent).
|
||||
- SSO-synced rows (source='feishu'/'wecom'/...) are untouched — their
|
||||
external_id is the authoritative external employee ID.
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = 'f012_backfill_local_external_id'
|
||||
down_revision: Union[str, Sequence[str], None] = 'f011_backfill_create_knowledge_web_menu'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _table_exists(table_name: str) -> bool:
|
||||
conn = op.get_bind()
|
||||
result = conn.execute(
|
||||
sa.text(
|
||||
'SELECT COUNT(*) FROM information_schema.TABLES '
|
||||
'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t',
|
||||
),
|
||||
{'t': table_name},
|
||||
)
|
||||
return result.scalar() > 0
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if not _table_exists('user'):
|
||||
return
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE `user`
|
||||
SET external_id = user_name
|
||||
WHERE external_id IS NULL
|
||||
AND source = 'local'
|
||||
AND `delete` = 0
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Intentional no-op: clearing external_id would re-break login for users
|
||||
# who have since logged in or been referenced by newer flows.
|
||||
pass
|
||||
@@ -118,6 +118,11 @@ class UserService:
|
||||
user = User(
|
||||
user_name=req_data.user_name,
|
||||
password=cls.decrypt_md5_password(req_data.password),
|
||||
source='local',
|
||||
# Default external_id to user_name so password login (which queries
|
||||
# external_id only since 94323e3ec) works out of the box. SSO-synced
|
||||
# users set their own external_id via org_sync, not through here.
|
||||
external_id=req_data.user_name,
|
||||
)
|
||||
group_ids = []
|
||||
role_ids = []
|
||||
|
||||
Reference in New Issue
Block a user