- 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个测试全部通过
251 lines
7.2 KiB
Python
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
|