Files
opsxsql/backend/app/api/v1/endpoints/instance_group.py
T
logan 765d27f1e3 feat: 实例权限组系统 — 完整CRUD + 实例/用户关联 + 前端管理页面
后端:
- InstanceGroup 模型 (name/db_type/permissions JSON/is_active)
- user_groups 关联表 (用户-权限组 M2M + 权限覆盖)
- Instance 模型增加 group_id FK
- User 模型增加 groups 关系
- API: CRUD + 实例关联 + 用户分配 + 权限组过滤
- 15 个新测试 (CRUD/关联/权限检查/参数校验) — 217/217 passing

前端:
- InstanceGroupPage: 左右布局(列表+详情)
- 权限配置: 5类17项权限勾选 (查询/DDL/工单/数据操作/管理)
- 实例关联/用户关联弹框
- 侧边栏增加权限组菜单 (数据源分组下)
- 路由 + 权限注册
2026-05-23 00:09:34 +08:00

331 lines
10 KiB
Python

import json
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select, func as sa_func
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import get_current_user, require_role
from app.models.user import User
from app.models.instance import Instance
from app.models.instance_group import InstanceGroup, user_groups
from app.schemas.instance_group import (
InstanceGroupCreate, InstanceGroupUpdate,
InstanceGroupResponse, InstanceGroupDetailResponse,
InstanceAssignRequest, UserAssignRequest, UserGroupUpdateRequest,
)
router = APIRouter(prefix="/instance-groups", tags=["权限组管理"])
def _serialize_group(group: InstanceGroup, instance_count: int = 0, user_count: int = 0) -> dict:
return {
"id": group.id,
"name": group.name,
"description": group.description,
"db_type": group.db_type,
"permissions": group.get_permissions(),
"is_active": group.is_active,
"created_at": group.created_at,
"updated_at": group.updated_at,
"instance_count": instance_count,
"user_count": user_count,
}
def _serialize_group_detail(group: InstanceGroup) -> dict:
d = _serialize_group(
group,
instance_count=len(group.instances) if group.instances else 0,
user_count=len(group.users) if group.users else 0,
)
d["instances"] = [
{
"id": inst.id,
"name": inst.name,
"db_type": inst.db_type,
"host": inst.host,
"port": inst.port,
"env": inst.env,
"status": inst.status,
}
for inst in (group.instances or [])
]
d["users"] = [
{
"id": u.id,
"username": u.username,
"display_name": u.display_name,
"role": u.role,
}
for u in (group.users or [])
]
return d
@router.get("", response_model=List[InstanceGroupResponse])
async def list_instance_groups(
db_type: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
stmt = select(InstanceGroup).order_by(InstanceGroup.id)
if db_type:
stmt = stmt.where(InstanceGroup.db_type == db_type)
result = await db.execute(stmt)
groups = result.scalars().all()
return [_serialize_group_detail(g) for g in groups]
@router.post("", response_model=InstanceGroupResponse, status_code=201)
async def create_instance_group(
data: InstanceGroupCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
# Check name uniqueness
existing = await db.execute(
select(InstanceGroup).where(InstanceGroup.name == data.name)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="权限组名称已存在")
group = InstanceGroup(
name=data.name,
description=data.description,
db_type=data.db_type,
permissions=json.dumps(data.permissions, ensure_ascii=False) if data.permissions else None,
is_active=data.is_active,
)
db.add(group)
await db.commit()
await db.refresh(group)
return _serialize_group(group)
@router.get("/{group_id}", response_model=InstanceGroupDetailResponse)
async def get_instance_group(
group_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
raise HTTPException(status_code=404, detail="权限组不存在")
return _serialize_group_detail(group)
@router.put("/{group_id}", response_model=InstanceGroupResponse)
async def update_instance_group(
group_id: int,
data: InstanceGroupUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
raise HTTPException(status_code=404, detail="权限组不存在")
update_data = data.model_dump(exclude_unset=True)
if "name" in update_data:
# Check uniqueness
existing = await db.execute(
select(InstanceGroup).where(
InstanceGroup.name == update_data["name"],
InstanceGroup.id != group_id,
)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="权限组名称已存在")
if "permissions" in update_data:
update_data["permissions"] = (
json.dumps(update_data["permissions"], ensure_ascii=False)
if update_data["permissions"]
else None
)
for key, value in update_data.items():
setattr(group, key, value)
await db.commit()
await db.refresh(group)
return _serialize_group(group)
@router.delete("/{group_id}")
async def delete_instance_group(
group_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
raise HTTPException(status_code=404, detail="权限组不存在")
# Detach instances
inst_result = await db.execute(
select(Instance).where(Instance.group_id == group_id)
)
for inst in inst_result.scalars().all():
inst.group_id = None
await db.delete(group)
await db.commit()
return {"message": "权限组已删除"}
@router.post("/{group_id}/instances")
async def assign_instances(
group_id: int,
data: InstanceAssignRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
raise HTTPException(status_code=404, detail="权限组不存在")
for inst_id in data.instance_ids:
inst_result = await db.execute(
select(Instance).where(Instance.id == inst_id)
)
inst = inst_result.scalar_one_or_none()
if inst:
inst.group_id = group_id
await db.commit()
return {"message": "实例已关联"}
@router.delete("/{group_id}/instances/{instance_id}")
async def remove_instance(
group_id: int,
instance_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
inst_result = await db.execute(
select(Instance).where(Instance.id == instance_id, Instance.group_id == group_id)
)
inst = inst_result.scalar_one_or_none()
if not inst:
raise HTTPException(status_code=404, detail="实例未关联该权限组")
inst.group_id = None
await db.commit()
return {"message": "实例已取消关联"}
@router.get("/{group_id}/users")
async def list_group_users(
group_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
raise HTTPException(status_code=404, detail="权限组不存在")
# Query user_groups for permissions override
ug_result = await db.execute(
select(user_groups).where(user_groups.c.group_id == group_id)
)
ug_rows = ug_result.all()
perm_map = {}
for row in ug_rows:
perms = json.loads(row.permissions) if row.permissions else None
perm_map[row.user_id] = perms
users_data = []
for u in (group.users or []):
users_data.append({
"id": u.id,
"username": u.username,
"display_name": u.display_name,
"role": u.role,
"permissions": perm_map.get(u.id),
})
return users_data
@router.post("/{group_id}/users")
async def assign_users(
group_id: int,
data: UserAssignRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == group_id)
)
group = result.scalar_one_or_none()
if not group:
raise HTTPException(status_code=404, detail="权限组不存在")
perm_json = json.dumps(data.permissions, ensure_ascii=False) if data.permissions else None
for user_id in data.user_ids:
# Check if already assigned
check = await db.execute(
select(user_groups).where(
user_groups.c.user_id == user_id,
user_groups.c.group_id == group_id,
)
)
if not check.first():
await db.execute(
user_groups.insert().values(
user_id=user_id,
group_id=group_id,
permissions=perm_json,
)
)
await db.commit()
return {"message": "用户已分配"}
@router.put("/users/{user_id}/groups")
async def update_user_groups(
user_id: int,
data: UserGroupUpdateRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
user_result = await db.execute(
select(User).where(User.id == user_id)
)
user = user_result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
# Remove existing group associations
await db.execute(
user_groups.delete().where(user_groups.c.user_id == user_id)
)
# Add new ones
for gid in data.group_ids:
group_result = await db.execute(
select(InstanceGroup).where(InstanceGroup.id == gid)
)
if group_result.scalar_one_or_none():
await db.execute(
user_groups.insert().values(user_id=user_id, group_id=gid)
)
await db.commit()
return {"message": "用户权限组已更新"}