267 lines
8.7 KiB
Python
267 lines
8.7 KiB
Python
"""新增场外 Worker 最小权限技术账号。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.engine import Connection
|
|
|
|
from alembic import op
|
|
|
|
revision = "20260910_offsite_worker_identity"
|
|
down_revision = "20260910_promotion_material"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
WORKER_USER_NO = "OFFSITE-WORKER"
|
|
WORKER_USERNAME = "offsite_worker"
|
|
WORKER_PASSWORD_HASH = "!worker-only-no-password-login!"
|
|
OPERATOR_ROLE_CODE = "operator"
|
|
OFFSITE_WRITE_PERMISSION = "offsite:write"
|
|
|
|
|
|
def upgrade() -> None:
|
|
connection = op.get_bind()
|
|
now = datetime.now(UTC).replace(tzinfo=None)
|
|
worker_user_id = _ensure_worker_user(connection, now)
|
|
role_id = _ensure_operator_role(connection, now)
|
|
permission_id = _ensure_offsite_write_permission(connection, now)
|
|
_ensure_operator_role_is_minimal(connection, role_id)
|
|
_ensure_role_permission(connection, role_id, permission_id, worker_user_id, now)
|
|
_ensure_user_role(connection, worker_user_id, role_id, now)
|
|
_write_audit(connection, worker_user_id, now)
|
|
|
|
|
|
def downgrade() -> None:
|
|
raise RuntimeError("场外 Worker 技术账号属于受控身份数据,禁止自动删除")
|
|
|
|
|
|
def _ensure_worker_user(connection: Connection, now: datetime) -> int:
|
|
rows = connection.execute(
|
|
text(
|
|
"""
|
|
SELECT id, user_no, username, user_type, employee_role, status
|
|
FROM sys_user
|
|
WHERE user_no=:user_no OR username=:username
|
|
"""
|
|
),
|
|
{"user_no": WORKER_USER_NO, "username": WORKER_USERNAME},
|
|
).mappings().all()
|
|
if len(rows) > 1:
|
|
raise RuntimeError("场外 Worker 技术账号的 user_no 与 username 分属不同用户")
|
|
if rows:
|
|
row = rows[0]
|
|
_assert_worker_user(row)
|
|
return int(row["id"])
|
|
worker_user_id = _next_user_id(connection)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO sys_user
|
|
(id, user_no, username, email, password_hash, user_type, employee_role,
|
|
professional_investor_status, fund_account_status, status, created_at, updated_at)
|
|
VALUES
|
|
(:id, :user_no, :username, NULL, :password_hash, '员工', :employee_role,
|
|
'未申请', '未开户', '正常', :now, :now)
|
|
"""
|
|
),
|
|
{
|
|
"id": worker_user_id,
|
|
"user_no": WORKER_USER_NO,
|
|
"username": WORKER_USERNAME,
|
|
"password_hash": WORKER_PASSWORD_HASH,
|
|
"employee_role": OPERATOR_ROLE_CODE,
|
|
"now": now,
|
|
},
|
|
)
|
|
return worker_user_id
|
|
|
|
|
|
def _assert_worker_user(row: Any) -> None:
|
|
if row["user_no"] != WORKER_USER_NO or row["username"] != WORKER_USERNAME:
|
|
raise RuntimeError("场外 Worker 技术账号唯一标识冲突")
|
|
if row["user_type"] != "员工" or row["status"] != "正常":
|
|
raise RuntimeError("场外 Worker 技术账号不是正常员工账号")
|
|
if row["employee_role"] != OPERATOR_ROLE_CODE:
|
|
raise RuntimeError("场外 Worker 技术账号岗位缓存不是 operator")
|
|
|
|
|
|
def _next_user_id(connection: Connection) -> int:
|
|
value = connection.scalar(text("SELECT COALESCE(MAX(id), 1000000000000000) + 1 FROM sys_user"))
|
|
return int(value)
|
|
|
|
|
|
def _ensure_operator_role(connection: Connection, now: datetime) -> int:
|
|
row = connection.execute(
|
|
text("SELECT id, status FROM sys_role WHERE role_code=:code"),
|
|
{"code": OPERATOR_ROLE_CODE},
|
|
).mappings().first()
|
|
if row is not None:
|
|
if row["status"] != "active":
|
|
raise RuntimeError("operator 角色未启用,拒绝绑定场外 Worker")
|
|
return int(row["id"])
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO sys_role (role_code, role_name, status, created_at, updated_at)
|
|
VALUES (:code, '运营人员', 'active', :now, :now)
|
|
"""
|
|
),
|
|
{"code": OPERATOR_ROLE_CODE, "now": now},
|
|
)
|
|
role_id = connection.scalar(
|
|
text("SELECT id FROM sys_role WHERE role_code=:code"),
|
|
{"code": OPERATOR_ROLE_CODE},
|
|
)
|
|
return int(role_id)
|
|
|
|
|
|
def _ensure_offsite_write_permission(connection: Connection, now: datetime) -> int:
|
|
row = connection.execute(
|
|
text("SELECT id FROM sys_permission WHERE permission_code=:code"),
|
|
{"code": OFFSITE_WRITE_PERMISSION},
|
|
).mappings().first()
|
|
if row is not None:
|
|
return int(row["id"])
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO sys_permission
|
|
(permission_code, resource, action, data_scope, field_policy, created_at, updated_at)
|
|
VALUES (:code, 'offsite_fund', 'write', 'all', NULL, :now, :now)
|
|
"""
|
|
),
|
|
{"code": OFFSITE_WRITE_PERMISSION, "now": now},
|
|
)
|
|
permission_id = connection.scalar(
|
|
text("SELECT id FROM sys_permission WHERE permission_code=:code"),
|
|
{"code": OFFSITE_WRITE_PERMISSION},
|
|
)
|
|
return int(permission_id)
|
|
|
|
|
|
def _ensure_operator_role_is_minimal(connection: Connection, role_id: int) -> None:
|
|
rows = connection.execute(
|
|
text(
|
|
"""
|
|
SELECT p.permission_code
|
|
FROM sys_role_permission rp
|
|
JOIN sys_permission p ON p.id=rp.permission_id
|
|
WHERE rp.role_id=:role_id
|
|
"""
|
|
),
|
|
{"role_id": role_id},
|
|
).mappings().all()
|
|
permission_codes = {str(row["permission_code"]) for row in rows}
|
|
extra_permissions = permission_codes - {OFFSITE_WRITE_PERMISSION}
|
|
if extra_permissions:
|
|
raise RuntimeError("operator 角色已有额外权限,拒绝作为场外 Worker 最小权限角色")
|
|
|
|
|
|
def _ensure_role_permission(
|
|
connection: Connection,
|
|
role_id: int,
|
|
permission_id: int,
|
|
worker_user_id: int,
|
|
now: datetime,
|
|
) -> None:
|
|
exists = connection.scalar(
|
|
text(
|
|
"""
|
|
SELECT COUNT(*)
|
|
FROM sys_role_permission
|
|
WHERE role_id=:role_id AND permission_id=:permission_id
|
|
"""
|
|
),
|
|
{"role_id": role_id, "permission_id": permission_id},
|
|
)
|
|
if int(exists or 0) > 0:
|
|
return
|
|
other_user_count = connection.scalar(
|
|
text(
|
|
"""
|
|
SELECT COUNT(*)
|
|
FROM sys_user_role
|
|
WHERE role_id=:role_id AND user_id<>:worker_user_id
|
|
"""
|
|
),
|
|
{"role_id": role_id, "worker_user_id": worker_user_id},
|
|
)
|
|
if int(other_user_count or 0) > 0:
|
|
raise RuntimeError("operator 角色已绑定其它用户,拒绝自动扩大 offsite:write 权限")
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO sys_role_permission (role_id, permission_id, created_at)
|
|
VALUES (:role_id, :permission_id, :now)
|
|
"""
|
|
),
|
|
{"role_id": role_id, "permission_id": permission_id, "now": now},
|
|
)
|
|
|
|
|
|
def _ensure_user_role(
|
|
connection: Connection, worker_user_id: int, role_id: int, now: datetime
|
|
) -> None:
|
|
exists = connection.scalar(
|
|
text(
|
|
"""
|
|
SELECT COUNT(*)
|
|
FROM sys_user_role
|
|
WHERE user_id=:user_id AND role_id=:role_id
|
|
"""
|
|
),
|
|
{"user_id": worker_user_id, "role_id": role_id},
|
|
)
|
|
if int(exists or 0) > 0:
|
|
return
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO sys_user_role (user_id, role_id, assigned_at, expires_at)
|
|
VALUES (:user_id, :role_id, :now, NULL)
|
|
"""
|
|
),
|
|
{"user_id": worker_user_id, "role_id": role_id, "now": now},
|
|
)
|
|
|
|
|
|
def _write_audit(connection: Connection, worker_user_id: int, now: datetime) -> None:
|
|
exists = connection.scalar(
|
|
text(
|
|
"""
|
|
SELECT COUNT(*)
|
|
FROM interaction_audit
|
|
WHERE portal='migration'
|
|
AND action_type='offsite.worker_identity_seeded'
|
|
AND JSON_UNQUOTE(JSON_EXTRACT(detail, '$.worker_user_id'))=:worker_user_id
|
|
"""
|
|
),
|
|
{"worker_user_id": str(worker_user_id)},
|
|
)
|
|
if int(exists or 0) > 0:
|
|
return
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO interaction_audit
|
|
(actor_type, actor_id, target_customer_id, session_id, portal, action_type, detail,
|
|
created_at)
|
|
VALUES
|
|
('system', NULL, NULL, NULL, 'migration',
|
|
'offsite.worker_identity_seeded',
|
|
JSON_OBJECT('worker_user_id', :worker_user_id, 'role', :role_code,
|
|
'permission', :permission_code),
|
|
:now)
|
|
"""
|
|
),
|
|
{
|
|
"worker_user_id": worker_user_id,
|
|
"role_code": OPERATOR_ROLE_CODE,
|
|
"permission_code": OFFSITE_WRITE_PERMISSION,
|
|
"now": now,
|
|
},
|
|
)
|