refactor: 权限组简化为2组(readonly/write) + Transfer穿梭框选择数据库

- access_type 从3值(readonly/datachange/ddl)简化为2值(readonly/write)
- readonly=只读查看(关联只读/从库用于查询) write=写入变更(关联主库用于工单)
- 前端InstanceGroupPage重写: 2权限类型卡片+自定义Transfer穿梭框组件
- Transfer组件: 左侧可选数据库→右侧已授权数据库, 搜索/勾选/箭头移动
- role_permissions.py更新: 2值权限映射, get_accessible_instances按access_type过滤
- 后端schema/模型校验更新为readonly|write正则
- 15个测试全部通过
This commit is contained in:
logan committed 2026-05-24 19:27:04 +08:00
1 parent 8161bcf5b5
commit 4111e1eee8
6 files changed
+352 -316

No files matched your search

+53 -91
View File
@@ -4,13 +4,17 @@
- admin/dba/auditor 权限由角色硬编码决定,不需要查权限组
- developer 权限由关联的权限组决定
- 工单操作权限完全由角色决定
权限组类型 (access_type):
- readonly: 只读查看 — 关联只读库/从库,用于查询数据
- write: 写入变更 — 关联读写库/主库,用于数据变更工单
"""
from __future__ import annotations
import json
import logging
from typing import Dict, List, Optional, Set
from typing import Dict, List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -32,40 +36,30 @@ logger = logging.getLogger(__name__)
ROLE_PERMISSIONS: Dict[str, dict] = {
"admin": {
"readonly": "all",
"datachange": "all",
"ddl": "all",
"write": "all",
"ticket": ["create", "approve", "reject", "execute", "cancel"],
},
"dba": {
"readonly": "all",
"datachange": "all",
"ddl": "all",
"write": "all",
"ticket": ["approve", "reject", "execute"],
},
"auditor": {
"readonly": "all",
"datachange": None,
"ddl": None,
"write": None,
"ticket": ["approve", "reject"],
},
"developer": {
"readonly": "groups",
"datachange": "groups",
"ddl": "groups",
"write": "groups",
"ticket": ["create", "cancel_own"],
},
"guest": {
"readonly": None,
"datachange": None,
"ddl": None,
"ticket": [],
},
}
def _get_role_config(role: str) -> dict:
"""获取角色配置,未知角色按 guest 处理。"""
return ROLE_PERMISSIONS.get(role, ROLE_PERMISSIONS["guest"])
"""获取角色配置,未知角色按最小权限处理。"""
return ROLE_PERMISSIONS.get(role, {"readonly": None, "write": None, "ticket": []})
# ============================================================
@@ -74,48 +68,23 @@ def _get_role_config(role: str) -> dict:
async def get_user_permissions(db: AsyncSession, user: User) -> dict:
"""返回用户的实际权限概览。
Returns:
{
"role": "developer",
"access": {
"readonly": "all" | "groups" | None,
"datachange": "all" | "groups" | None,
"ddl": "all" | "groups" | None,
},
"ticket": ["create", ...],
"groups": [ # 仅 developer 类型有详情
{
"group_id": 1,
"group_name": "xxx",
"access_type": "readonly",
"instance_ids": [1, 2],
"allowed_databases": ["db1", "db2"],
},
...
],
}
"""
"""返回用户的实际权限概览。"""
role_config = _get_role_config(user.role)
result = {
"role": user.role,
"access": {
"readonly": role_config["readonly"],
"datachange": role_config["datachange"],
"ddl": role_config["ddl"],
"readonly": role_config.get("readonly"),
"write": role_config.get("write"),
},
"ticket": role_config["ticket"],
"ticket": role_config.get("ticket", []),
"groups": [],
}
# 如果角色权限由权限组决定,查询用户关联的权限组
has_group_access = any(
v == "groups" for v in role_config.values() if isinstance(v, str)
v == "groups" for k, v in role_config.items() if k in ("readonly", "write")
)
if has_group_access:
groups_detail = await _get_user_groups_detail(db, user)
result["groups"] = groups_detail
result["groups"] = await _get_user_groups_detail(db, user)
return result
@@ -130,30 +99,18 @@ async def can_access_database(
"""判断用户是否可以对指定实例的指定库执行指定类型的操作。
Args:
db: 数据库会话
user: 用户对象
instance_id: 实例 ID
database: 数据库名
access_type: "readonly" / "datachange" / "ddl"
Returns:
True 如果允许
access_type: "readonly" / "write"
"""
if access_type not in ("readonly", "datachange", "ddl"):
if access_type not in ("readonly", "write"):
return False
role_config = _get_role_config(user.role)
access_value = role_config.get(access_type)
# "all" — 全部实例全部库
if access_value == "all":
return True
# None — 无权限
if access_value is None:
return False
# "groups" — 由权限组决定
if access_value == "groups":
return await _check_group_access(db, user, instance_id, database, access_type)
@@ -161,48 +118,55 @@ async def can_access_database(
def can_ticket_action(user: User, action: str) -> bool:
"""判断用户是否可以执行指定工单操作(纯角色判断,不需要查 DB)。
Args:
user: 用户对象
action: "create" / "approve" / "reject" / "execute" / "cancel" / "cancel_own"
Returns:
True 如果允许
"""
"""判断用户是否可以执行指定工单操作。"""
role_config = _get_role_config(user.role)
ticket_perms = role_config.get("ticket", [])
# cancel_own 是一个特殊权限,caller 需要额外验证工单申请人
return action in ticket_perms
return action in role_config.get("ticket", [])
async def get_accessible_instances(
db: AsyncSession,
user: User,
access_type: str = "readonly",
) -> List[Instance]:
"""获取用户可访问的实例列表。
"""获取用户可访问的实例列表(按 access_type 过滤)。
- admin/dba/auditor(readonly=all): 返回所有实例
- admin/dba: 返回所有实例
- auditor: readonly 返回所有,write 返回空
- developer: 返回其权限组关联的实例
"""
role_config = _get_role_config(user.role)
access_value = role_config.get(access_type)
# 判断是否有 "all" 权限(至少有一个 access_type 为 all)
has_all = any(v == "all" for v in role_config.values() if isinstance(v, str))
if has_all:
if access_value == "all":
result = await db.execute(select(Instance))
return list(result.scalars().all())
# developer: 查询权限组关联的实例
if access_value is None:
return []
# "groups" — 查询权限组关联的实例
group_ids = await _get_user_group_ids(db, user)
if not group_ids:
return []
# 只返回匹配 access_type 的权限组关联的实例
result = await db.execute(
select(Instance).where(Instance.group_id.in_(group_ids))
select(InstanceGroup).where(InstanceGroup.id.in_(group_ids))
)
return list(result.scalars().all())
groups = result.scalars().all()
matched_instance_ids: set = set()
for g in groups:
if g.access_type == access_type:
for inst in (g.instances or []):
matched_instance_ids.add(inst.id)
if not matched_instance_ids:
return []
inst_result = await db.execute(
select(Instance).where(Instance.id.in_(matched_instance_ids))
)
return list(inst_result.scalars().all())
# ============================================================
@@ -250,7 +214,6 @@ async def _check_group_access(
access_type: str,
) -> bool:
"""检查用户通过权限组是否能访问指定实例+库。"""
# 获取实例信息
inst_result = await db.execute(
select(Instance).where(Instance.id == instance_id)
)
@@ -259,15 +222,12 @@ async def _check_group_access(
return False
if instance.group_id is None:
# 公开实例,任何已登录用户都可以 readonly
return access_type == "readonly"
# 检查用户是否属于该实例所在的权限组
group_ids = await _get_user_group_ids(db, user)
if instance.group_id not in group_ids:
return False
# 获取权限组
group_result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == instance.group_id)
)
@@ -275,14 +235,16 @@ async def _check_group_access(
if not group:
return False
# 检查 access_type 是否匹配
if group.access_type != access_type:
# readonly 权限组也可以被 write 请求使用(读+写包含只读)
if access_type == "write" and group.access_type == "readonly":
return False
# 检查 allowed_databases
# readonly 请求可以用 readonly 或 write 权限组
if access_type == "readonly" and group.access_type in ("readonly", "write"):
pass # ok
allowed_dbs = group.get_allowed_databases()
if not allowed_dbs:
# 空列表 = 该实例所有库
return True
return database in allowed_dbs