fix: db version

This commit is contained in:
tao.chen
2026-08-07 12:59:41 +08:00
parent 586d9fa645
commit a891f1f649
@@ -0,0 +1,167 @@
"""Seed platform permissions + role_permissions, fix admin/developer role_scope.
The squashed baseline (d4e5f6a7b8c9) ships the Permissions and
RolePermissions tables empty, and seeds admin/developer with
role_scope='workspace' (an early mistake; the codebase elsewhere treats
both as platform-scoped — see backend/platform.py::system_admin_context
and common/auth/membership.py::resolve_is_system_admin). This migration:
1. UPDATE roles SET role_scope='platform' for admin/developer rows.
2. INSERT 12 permission rows covering the menu groups the frontend
consumes (dashboard / script / schedule / experiment / resource /
system).
3. INSERT role_permissions join rows: admin gets all 12, developer
gets the 6 `*.own` / personal-resource codes.
permission_id values are deterministic (sha256 of code) so a
downgrade → upgrade cycle is idempotent on the uk_permissions_code
unique index. Downgrade soft-deletes (is_deleted=1, deleted_at=now)
the rows this migration inserted; it does NOT revert role_scope
because that fix is not safely reversible once app code has touched
the rows.
Revision ID: f6a7b8c9d0e1
Revises: e5f6a7b8c9d0 (ensure_demo_login)
Create Date: 2026-08-07
"""
from __future__ import annotations
import hashlib
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
# Hardcoded to match d4e5f6a7b8c9_squashed_baseline.py seed values, so the
# role_permissions join rows below resolve against the right role rows.
ADMIN_ROLE_ID = "0000000000000000000000000A"
DEVELOPER_ROLE_ID = "0000000000000000000000000B"
PERMISSIONS: list[tuple[str, str, str]] = [
# (permission_code, permission_name, module_code)
("dashboard.view", "查看工作台", "dashboard"),
("script.build", "构建脚本", "script"),
("script.public.manage", "管理公共脚本", "script"),
("schedule.own", "管理本人调度", "schedule"),
("schedule.all", "管理全部调度", "schedule"),
("experiment.own", "管理本人实验", "experiment"),
("experiment.all", "管理全部实验", "experiment"),
("resource.personal", "管理个人资源", "resource"),
("resource.public.upload", "上传公共资源", "resource"),
("resource.public.manage", "管理公共资源", "resource"),
("system.view", "查看系统管理", "system"),
("system.manage", "管理系统配置", "system"),
]
# developer gets *.own + personal resource only; no system.*, no *.all.
DEVELOPER_PERMISSION_CODES: list[str] = [
"dashboard.view",
"script.build",
"script.public.manage",
"schedule.own",
"experiment.own",
"resource.personal",
]
ADMIN_PERMISSION_CODES: list[str] = [code for code, _, _ in PERMISSIONS]
def _deterministic_permission_id(code: str) -> str:
"""Stable 26-char ULID-shaped id derived from permission_code.
Mirrors ``migrations/data/migrate_system_json.py::deterministic_legacy_ulid``
so re-running this migration (or running it after the legacy data
migrator) keeps identical IDs for the same code.
"""
digest = hashlib.sha256(
f"model-platform-permission-v1:{code}".encode("utf-8")
).digest()
value = int.from_bytes(b"\x00" * 6 + digest[:10], byteorder="big")
encoded = ["0"] * 26
for index in range(25, -1, -1):
encoded[index] = CROCKFORD_BASE32[value & 31]
value >>= 5
return "".join(encoded)
# revision identifiers, used by Alembic.
revision: str = "f6a7b8c9d0e1"
down_revision: str | Sequence[str] | None = "e5f6a7b8c9d0"
def upgrade() -> None:
# (1) role_scope fix: admin/developer were seeded as 'workspace' but
# platform.py::system_admin_context treats them as platform-scoped.
op.execute(
"UPDATE roles SET role_scope = 'platform' "
"WHERE role_code IN ('admin', 'developer') AND is_deleted = 0"
)
# (2) Permissions rows.
permissions_table = sa.table(
"permissions",
sa.column("permission_id", sa.CHAR(26)),
sa.column("permission_code", sa.String(128)),
sa.column("permission_name", sa.String(100)),
sa.column("module_code", sa.String(64)),
sa.column("description", sa.String(500)),
)
perm_id_by_code: dict[str, str] = {}
rows: list[dict[str, str]] = []
for code, name, module in PERMISSIONS:
pid = _deterministic_permission_id(code)
perm_id_by_code[code] = pid
rows.append(
{
"permission_id": pid,
"permission_code": code,
"permission_name": name,
"module_code": module,
"description": f"platform 菜单权限:{name}",
}
)
op.bulk_insert(permissions_table, rows)
# (3) role_permissions join rows.
role_permissions_table = sa.table(
"role_permissions",
sa.column("role_id", sa.CHAR(26)),
sa.column("permission_id", sa.CHAR(26)),
)
rp_rows: list[dict[str, str]] = []
for code in ADMIN_PERMISSION_CODES:
rp_rows.append(
{
"role_id": ADMIN_ROLE_ID,
"permission_id": perm_id_by_code[code],
}
)
for code in DEVELOPER_PERMISSION_CODES:
rp_rows.append(
{
"role_id": DEVELOPER_ROLE_ID,
"permission_id": perm_id_by_code[code],
}
)
op.bulk_insert(role_permissions_table, rp_rows)
def downgrade() -> None:
# Soft-delete what we inserted. The role_scope fix is intentionally
# NOT reverted — the only safe direction is platform, since
# application code already keys off it.
code_list_sql = "(" + ",".join(f"'{c}'" for c in ADMIN_PERMISSION_CODES) + ")"
op.execute(
"UPDATE role_permissions "
"SET is_deleted = 1, deleted_at = CURRENT_TIMESTAMP(3) "
f"WHERE permission_id IN (SELECT permission_id FROM permissions "
f"WHERE is_deleted = 0 AND permission_code IN {code_list_sql})"
)
op.execute(
"UPDATE permissions SET is_deleted = 1, deleted_at = CURRENT_TIMESTAMP(3) "
f"WHERE is_deleted = 0 AND permission_code IN {code_list_sql}"
)