refactor: 权限组从一组一类型改为一组同时包含readonly+write实例
- 新增 instance_group_members 中间表关联权限组和实例 - InstanceGroup 模型移除旧的 instances relationship,改为通过中间表查询 - Instance 模型保留 group_id FK(兼容)但移除 group relationship - Schema 新增 readonly_instance_ids / write_instance_ids 字段 - InstanceGroupResponse 改为 readonly_instance_count + write_instance_count - InstanceGroupDetailResponse 返回 readonly_instances + write_instances - InstanceAssignRequest 新增 access_type 参数 - API create/update 支持同步写入 instance_group_members - role_permissions 服务更新为使用中间表查询 - 新增迁移 SQL 并应用到 sqlite3.db - 测试全部通过 (20/20)
This commit is contained in:
1 parent
40868c61bf
commit
39d9600e82
8 files changed
+418
-97
No files matched your search
@@ -2,60 +2,79 @@ import json
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select, func as sa_func
|
||||
from sqlalchemy import select, delete as sa_delete, 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.models.instance_group import InstanceGroup, user_groups, instance_group_members
|
||||
from app.schemas.instance_group import (
|
||||
InstanceGroupCreate, InstanceGroupUpdate,
|
||||
InstanceGroupResponse, InstanceGroupDetailResponse,
|
||||
InstanceAssignRequest, UserAssignRequest, UserGroupUpdateRequest,
|
||||
InstanceAssignRequest, InstanceRemoveRequest,
|
||||
UserAssignRequest, UserGroupUpdateRequest,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/instance-groups", tags=["权限组管理"])
|
||||
|
||||
INSTANCE_FIELDS = ["id", "name", "db_type", "host", "port", "env", "status"]
|
||||
|
||||
def _serialize_group(group: InstanceGroup, instance_count: int = 0, user_count: int = 0) -> dict:
|
||||
|
||||
def _serialize_instance(inst: Instance) -> dict:
|
||||
return {k: getattr(inst, k, None) for k in INSTANCE_FIELDS}
|
||||
|
||||
|
||||
async def _count_members(db: AsyncSession, group_id: int, access_type: str) -> int:
|
||||
result = await db.execute(
|
||||
select(sa_func.count()).select_from(instance_group_members).where(
|
||||
instance_group_members.c.group_id == group_id,
|
||||
instance_group_members.c.access_type == access_type,
|
||||
)
|
||||
)
|
||||
return result.scalar() or 0
|
||||
|
||||
|
||||
async def _get_member_instances(db: AsyncSession, group_id: int, access_type: str) -> List[dict]:
|
||||
"""查询权限组中指定 access_type 的实例列表。"""
|
||||
# Join instance_group_members with instances
|
||||
result = await db.execute(
|
||||
select(Instance, instance_group_members.c.access_type)
|
||||
.join(instance_group_members, Instance.id == instance_group_members.c.instance_id)
|
||||
.where(
|
||||
instance_group_members.c.group_id == group_id,
|
||||
instance_group_members.c.access_type == access_type,
|
||||
)
|
||||
)
|
||||
rows = result.all()
|
||||
return [_serialize_instance(row[0]) for row in rows]
|
||||
|
||||
|
||||
async def _serialize_group(group: InstanceGroup, db: AsyncSession) -> dict:
|
||||
ro_count = await _count_members(db, group.id, "readonly")
|
||||
wr_count = await _count_members(db, group.id, "write")
|
||||
user_count = len(group.users) if group.users else 0
|
||||
return {
|
||||
"id": group.id,
|
||||
"name": group.name,
|
||||
"description": group.description,
|
||||
"db_type": group.db_type,
|
||||
# Legacy field
|
||||
"permissions": group.get_permissions(),
|
||||
# New fields
|
||||
"access_type": group.access_type,
|
||||
"allowed_databases": group.get_allowed_databases(),
|
||||
"is_active": group.is_active,
|
||||
"created_at": group.created_at,
|
||||
"updated_at": group.updated_at,
|
||||
"instance_count": instance_count,
|
||||
"readonly_instance_count": ro_count,
|
||||
"write_instance_count": wr_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 [])
|
||||
]
|
||||
async def _serialize_group_detail(group: InstanceGroup, db: AsyncSession) -> dict:
|
||||
d = await _serialize_group(group, db)
|
||||
d["readonly_instances"] = await _get_member_instances(db, group.id, "readonly")
|
||||
d["write_instances"] = await _get_member_instances(db, group.id, "write")
|
||||
d["users"] = [
|
||||
{
|
||||
"id": u.id,
|
||||
@@ -68,6 +87,36 @@ def _serialize_group_detail(group: InstanceGroup) -> dict:
|
||||
return d
|
||||
|
||||
|
||||
async def _sync_members(
|
||||
db: AsyncSession,
|
||||
group_id: int,
|
||||
readonly_instance_ids: List[int],
|
||||
write_instance_ids: List[int],
|
||||
):
|
||||
"""Clear and rewrite all instance_group_members for a group."""
|
||||
await db.execute(
|
||||
sa_delete(instance_group_members).where(
|
||||
instance_group_members.c.group_id == group_id
|
||||
)
|
||||
)
|
||||
for inst_id in readonly_instance_ids:
|
||||
await db.execute(
|
||||
instance_group_members.insert().values(
|
||||
group_id=group_id,
|
||||
instance_id=inst_id,
|
||||
access_type="readonly",
|
||||
)
|
||||
)
|
||||
for inst_id in write_instance_ids:
|
||||
await db.execute(
|
||||
instance_group_members.insert().values(
|
||||
group_id=group_id,
|
||||
instance_id=inst_id,
|
||||
access_type="write",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=List[InstanceGroupResponse])
|
||||
async def list_instance_groups(
|
||||
db_type: Optional[str] = None,
|
||||
@@ -79,7 +128,7 @@ async def list_instance_groups(
|
||||
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]
|
||||
return [await _serialize_group_detail(g, db) for g in groups]
|
||||
|
||||
|
||||
@router.post("", response_model=InstanceGroupResponse, status_code=201)
|
||||
@@ -99,21 +148,23 @@ async def create_instance_group(
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
db_type=data.db_type,
|
||||
# New fields
|
||||
access_type=data.access_type,
|
||||
allowed_databases=(
|
||||
json.dumps(data.allowed_databases, ensure_ascii=False)
|
||||
if data.allowed_databases
|
||||
else None
|
||||
),
|
||||
# Legacy field (kept for compat)
|
||||
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)
|
||||
|
||||
# Write members
|
||||
await _sync_members(db, group.id, data.readonly_instance_ids, data.write_instance_ids)
|
||||
await db.commit()
|
||||
|
||||
return await _serialize_group(group, db)
|
||||
|
||||
|
||||
@router.get("/{group_id}", response_model=InstanceGroupDetailResponse)
|
||||
@@ -128,7 +179,7 @@ async def get_instance_group(
|
||||
group = result.scalar_one_or_none()
|
||||
if not group:
|
||||
raise HTTPException(status_code=404, detail="权限组不存在")
|
||||
return _serialize_group_detail(group)
|
||||
return await _serialize_group_detail(group, db)
|
||||
|
||||
|
||||
@router.put("/{group_id}", response_model=InstanceGroupResponse)
|
||||
@@ -173,11 +224,25 @@ async def update_instance_group(
|
||||
else None
|
||||
)
|
||||
|
||||
# Extract member lists before setting attributes
|
||||
ro_ids = update_data.pop("readonly_instance_ids", None)
|
||||
wr_ids = update_data.pop("write_instance_ids", None)
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(group, key, value)
|
||||
await db.commit()
|
||||
await db.refresh(group)
|
||||
return _serialize_group(group)
|
||||
|
||||
# Update members if provided
|
||||
if ro_ids is not None or wr_ids is not None:
|
||||
await _sync_members(
|
||||
db, group_id,
|
||||
ro_ids if ro_ids is not None else [],
|
||||
wr_ids if wr_ids is not None else [],
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return await _serialize_group(group, db)
|
||||
|
||||
|
||||
@router.delete("/{group_id}")
|
||||
@@ -193,12 +258,12 @@ async def delete_instance_group(
|
||||
if not group:
|
||||
raise HTTPException(status_code=404, detail="权限组不存在")
|
||||
|
||||
# Detach instances
|
||||
inst_result = await db.execute(
|
||||
select(Instance).where(Instance.group_id == group_id)
|
||||
# Delete members first
|
||||
await db.execute(
|
||||
sa_delete(instance_group_members).where(
|
||||
instance_group_members.c.group_id == group_id
|
||||
)
|
||||
)
|
||||
for inst in inst_result.scalars().all():
|
||||
inst.group_id = None
|
||||
|
||||
await db.delete(group)
|
||||
await db.commit()
|
||||
@@ -225,7 +290,22 @@ async def assign_instances(
|
||||
)
|
||||
inst = inst_result.scalar_one_or_none()
|
||||
if inst:
|
||||
inst.group_id = group_id
|
||||
# Check for duplicate
|
||||
existing = await db.execute(
|
||||
select(instance_group_members).where(
|
||||
instance_group_members.c.group_id == group_id,
|
||||
instance_group_members.c.instance_id == inst_id,
|
||||
instance_group_members.c.access_type == data.access_type,
|
||||
)
|
||||
)
|
||||
if not existing.first():
|
||||
await db.execute(
|
||||
instance_group_members.insert().values(
|
||||
group_id=group_id,
|
||||
instance_id=inst_id,
|
||||
access_type=data.access_type,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
return {"message": "实例已关联"}
|
||||
|
||||
@@ -234,16 +314,22 @@ async def assign_instances(
|
||||
async def remove_instance(
|
||||
group_id: int,
|
||||
instance_id: int,
|
||||
access_type: Optional[str] = None,
|
||||
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)
|
||||
conditions = [
|
||||
instance_group_members.c.group_id == group_id,
|
||||
instance_group_members.c.instance_id == instance_id,
|
||||
]
|
||||
if access_type:
|
||||
conditions.append(instance_group_members.c.access_type == access_type)
|
||||
|
||||
result = await db.execute(
|
||||
sa_delete(instance_group_members).where(*conditions)
|
||||
)
|
||||
inst = inst_result.scalar_one_or_none()
|
||||
if not inst:
|
||||
if result.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="实例未关联该权限组")
|
||||
inst.group_id = None
|
||||
await db.commit()
|
||||
return {"message": "实例已取消关联"}
|
||||
|
||||
|
||||
Reference in new issue
Block a user