Files
opsxsql/backend/app/services/role_permissions.py
T
logan 4111e1eee8 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个测试全部通过
2026-05-24 19:27:04 +08:00

251 lines
7.2 KiB
Python

"""角色权限服务 — 基于角色的权限查询。
权限逻辑:
- admin/dba/auditor 权限由角色硬编码决定,不需要查权限组
- developer 权限由关联的权限组决定
- 工单操作权限完全由角色决定
权限组类型 (access_type):
- readonly: 只读查看 — 关联只读库/从库,用于查询数据
- write: 写入变更 — 关联读写库/主库,用于数据变更工单
"""
from __future__ import annotations
import json
import logging
from typing import Dict, List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.models.instance import Instance
from app.models.instance_group import InstanceGroup, user_groups
logger = logging.getLogger(__name__)
# ============================================================
# 角色权限映射(硬编码)
# ============================================================
# access_type 值:
# "all" — 全部实例全部库
# "groups" — 由权限组决定
# None — 无权限
# ticket 值: list of allowed ticket actions
ROLE_PERMISSIONS: Dict[str, dict] = {
"admin": {
"readonly": "all",
"write": "all",
"ticket": ["create", "approve", "reject", "execute", "cancel"],
},
"dba": {
"readonly": "all",
"write": "all",
"ticket": ["approve", "reject", "execute"],
},
"auditor": {
"readonly": "all",
"write": None,
"ticket": ["approve", "reject"],
},
"developer": {
"readonly": "groups",
"write": "groups",
"ticket": ["create", "cancel_own"],
},
}
def _get_role_config(role: str) -> dict:
"""获取角色配置,未知角色按最小权限处理。"""
return ROLE_PERMISSIONS.get(role, {"readonly": None, "write": None, "ticket": []})
# ============================================================
# Public API
# ============================================================
async def get_user_permissions(db: AsyncSession, user: User) -> dict:
"""返回用户的实际权限概览。"""
role_config = _get_role_config(user.role)
result = {
"role": user.role,
"access": {
"readonly": role_config.get("readonly"),
"write": role_config.get("write"),
},
"ticket": role_config.get("ticket", []),
"groups": [],
}
has_group_access = any(
v == "groups" for k, v in role_config.items() if k in ("readonly", "write")
)
if has_group_access:
result["groups"] = await _get_user_groups_detail(db, user)
return result
async def can_access_database(
db: AsyncSession,
user: User,
instance_id: int,
database: str,
access_type: str,
) -> bool:
"""判断用户是否可以对指定实例的指定库执行指定类型的操作。
Args:
access_type: "readonly" / "write"
"""
if access_type not in ("readonly", "write"):
return False
role_config = _get_role_config(user.role)
access_value = role_config.get(access_type)
if access_value == "all":
return True
if access_value is None:
return False
if access_value == "groups":
return await _check_group_access(db, user, instance_id, database, access_type)
return False
def can_ticket_action(user: User, action: str) -> bool:
"""判断用户是否可以执行指定工单操作。"""
role_config = _get_role_config(user.role)
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 返回所有,write 返回空
- developer: 返回其权限组关联的实例
"""
role_config = _get_role_config(user.role)
access_value = role_config.get(access_type)
if access_value == "all":
result = await db.execute(select(Instance))
return list(result.scalars().all())
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(InstanceGroup).where(InstanceGroup.id.in_(group_ids))
)
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())
# ============================================================
# Internal helpers
# ============================================================
async def _get_user_group_ids(db: AsyncSession, user: User) -> List[int]:
"""获取用户关联的所有权限组 ID。"""
result = await db.execute(
select(user_groups.c.group_id).where(user_groups.c.user_id == user.id)
)
return [row[0] for row in result.all()]
async def _get_user_groups_detail(db: AsyncSession, user: User) -> List[dict]:
"""获取用户关联权限组的详细信息。"""
group_ids = await _get_user_group_ids(db, user)
if not group_ids:
return []
result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id.in_(group_ids))
)
groups = result.scalars().all()
details = []
for g in groups:
instance_ids = [inst.id for inst in (g.instances or [])]
details.append({
"group_id": g.id,
"group_name": g.name,
"access_type": g.access_type,
"instance_ids": instance_ids,
"allowed_databases": g.get_allowed_databases(),
})
return details
async def _check_group_access(
db: AsyncSession,
user: User,
instance_id: int,
database: str,
access_type: str,
) -> bool:
"""检查用户通过权限组是否能访问指定实例+库。"""
inst_result = await db.execute(
select(Instance).where(Instance.id == instance_id)
)
instance = inst_result.scalar_one_or_none()
if not instance:
return False
if instance.group_id is None:
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)
)
group = group_result.scalar_one_or_none()
if not group:
return False
# readonly 权限组也可以被 write 请求使用(读+写包含只读)
if access_type == "write" and group.access_type == "readonly":
return False
# 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