完全移除模块级权限系统,迁移至接口级权限

后端:
- 删除 StudyRolePermission 模型和 project_permissions API 文件
- 重写 project_permissions.py core,移除所有模块级权限函数
- 重写 permission_cache.py,移除模块级权限缓存逻辑
- 新增权限模板功能(PermissionTemplate 模型、API、服务层)
- 新增 permission_templates 数据库迁移
- 迁移 8 个模块(attachments、audit_logs、dashboard、faqs、
  faq_categories、fees_attachments、knowledge_notes、
  material_equipments、overview、subject_histories、subject_pds)
  至接口级权限检查
- 删除所有模块级权限相关测试文件,新增权限模板测试

前端:
- 删除 ProjectPermissions.vue 和 ProjectPermissionsModule.vue
- 重写 projectRoutePermissions.ts,改为基于接口级权限格式
- 更新 store/study.ts、router/index.ts、AuditLogs.vue、Projects.vue
  中的权限 API 调用,从 fetchProjectRolePermissions 改为
  fetchApiEndpointPermissions
- 清理 types/api.ts 中的旧模块级权限类型定义
- 新增 PermissionTemplateSelector.vue 组件
- 更新权限管理页面,移除模块级权限 tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cheng Zhou
2026-05-15 09:07:43 +08:00
parent 3b1bdc2070
commit 20ce6bccef
55 changed files with 1971 additions and 3030 deletions
+3 -4
View File
@@ -12,7 +12,7 @@ from sqlalchemy import delete as sa_delete, or_, select, update as sa_update
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_cra_site_scope
from app.core.project_permissions import role_has_project_permission
from app.core.project_permissions import role_has_api_permission
from app.crud import acknowledgement as acknowledgement_crud
from app.crud import distribution as distribution_crud
from app.crud import document as document_crud
@@ -75,9 +75,8 @@ async def _ensure_study_access(db: AsyncSession, trial_id: uuid.UUID, current_us
membership = await member_crud.get_member(db, trial_id, current_user.id)
if not membership or not membership.is_active:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
module = "file_versions"
permission_action = "read" if action in {"view", "ack"} else "write"
allowed = await role_has_project_permission(db, trial_id, membership.role_in_study, module, permission_action)
endpoint_key = "documents:read" if action in {"view", "ack"} else "documents:update"
allowed = await role_has_api_permission(db, trial_id, membership.role_in_study, endpoint_key, check_prerequisites=False)
if not allowed:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
return membership
@@ -0,0 +1,237 @@
from typing import Optional
from uuid import UUID
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.permission_template import PermissionTemplate, PermissionTemplateVersion, TemplateType
from app.models.api_endpoint_permission import ApiEndpointPermission
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS
from app.schemas.permission_template import PermissionTemplateCreate, PermissionTemplateUpdate
class PermissionTemplateService:
"""权限模板服务"""
@staticmethod
async def create_template(
db: AsyncSession,
payload: PermissionTemplateCreate,
created_by: UUID,
) -> PermissionTemplate:
"""创建权限模板"""
# 验证权限配置
await PermissionTemplateService._validate_permissions(payload.permissions)
template = PermissionTemplate(
name=payload.name,
description=payload.description,
template_type=payload.template_type,
is_system=False,
created_by=created_by,
permissions=payload.permissions,
tags=payload.tags,
category=payload.category,
recommended_roles=payload.recommended_roles,
)
db.add(template)
await db.flush() # 获取 template.id
# 创建版本 1
version = PermissionTemplateVersion(
template_id=template.id,
version=1,
permissions=payload.permissions,
change_log="初始版本",
)
db.add(version)
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def get_template(db: AsyncSession, template_id: UUID) -> Optional[PermissionTemplate]:
"""获取权限模板"""
result = await db.execute(
select(PermissionTemplate)
.options(selectinload(PermissionTemplate.versions))
.where(PermissionTemplate.id == template_id)
)
return result.scalar_one_or_none()
@staticmethod
async def list_templates(
db: AsyncSession,
template_type: Optional[TemplateType] = None,
category: Optional[str] = None,
skip: int = 0,
limit: int = 100,
) -> list[PermissionTemplate]:
"""列表权限模板"""
query = select(PermissionTemplate).options(selectinload(PermissionTemplate.versions))
if template_type:
query = query.where(PermissionTemplate.template_type == template_type)
if category:
query = query.where(PermissionTemplate.category == category)
query = query.offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
@staticmethod
async def update_template(
db: AsyncSession,
template_id: UUID,
payload: PermissionTemplateUpdate,
) -> PermissionTemplate:
"""更新权限模板"""
template = await PermissionTemplateService.get_template(db, template_id)
if not template:
raise ValueError(f"模板 {template_id} 不存在")
# 不允许修改系统预设模板
if template.is_system:
raise ValueError("不允许修改系统预设模板")
# 验证权限配置
if payload.permissions:
await PermissionTemplateService._validate_permissions(payload.permissions)
# 更新字段
if payload.name:
template.name = payload.name
if payload.description is not None:
template.description = payload.description
if payload.permissions:
# 加载 versions 以获取最新版本号
await db.refresh(template, ["versions"])
latest_version = max([v.version for v in template.versions], default=0)
new_version = PermissionTemplateVersion(
template_id=template.id,
version=latest_version + 1,
permissions=payload.permissions,
change_log="",
)
db.add(new_version)
template.permissions = payload.permissions
if payload.tags is not None:
template.tags = payload.tags
if payload.category is not None:
template.category = payload.category
if payload.recommended_roles is not None:
template.recommended_roles = payload.recommended_roles
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def delete_template(db: AsyncSession, template_id: UUID) -> None:
"""删除权限模板"""
template = await PermissionTemplateService.get_template(db, template_id)
if not template:
raise ValueError(f"模板 {template_id} 不存在")
# 不允许删除系统预设模板
if template.is_system:
raise ValueError("不允许删除系统预设模板")
await db.delete(template)
await db.commit()
@staticmethod
async def apply_template(
db: AsyncSession,
study_id: UUID,
template_id: UUID,
roles: Optional[list[str]] = None,
override: bool = True,
) -> dict:
"""应用权限模板到项目"""
template = await PermissionTemplateService.get_template(db, template_id)
if not template:
raise ValueError(f"模板 {template_id} 不存在")
# 确定要应用的角色
if roles is None:
if template.recommended_roles:
roles = template.recommended_roles.split(",")
else:
roles = list(template.permissions.keys())
# 删除现有权限(如果覆盖)
if override:
await db.execute(
delete(ApiEndpointPermission).where(
ApiEndpointPermission.study_id == study_id,
ApiEndpointPermission.role.in_(roles),
)
)
# 应用模板权限
for role in roles:
if role not in template.permissions:
continue
role_permissions = template.permissions[role]
for endpoint_key, allowed in role_permissions.items():
# 检查权限是否存在
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
continue
# 检查是否已存在
result = await db.execute(
select(ApiEndpointPermission).where(
ApiEndpointPermission.study_id == study_id,
ApiEndpointPermission.role == role,
ApiEndpointPermission.endpoint_key == endpoint_key,
)
)
perm = result.scalar_one_or_none()
if perm:
perm.allowed = allowed
else:
perm = ApiEndpointPermission(
study_id=study_id,
role=role,
endpoint_key=endpoint_key,
allowed=allowed,
)
db.add(perm)
await db.commit()
# 返回应用后的权限矩阵
from app.core.project_permissions import get_api_endpoint_permissions
permissions = await get_api_endpoint_permissions(db, study_id)
return {
"study_id": study_id,
"applied_roles": roles,
"permissions": permissions,
}
@staticmethod
async def _validate_permissions(permissions: dict) -> None:
"""验证权限配置"""
for role, role_permissions in permissions.items():
if not isinstance(role_permissions, dict):
raise ValueError(f"角色 {role} 的权限配置格式错误")
for endpoint_key, allowed in role_permissions.items():
if endpoint_key.startswith("_"): # 跳过元数据字段
continue
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
raise ValueError(f"权限操作 {endpoint_key} 不存在")
if not isinstance(allowed, bool):
raise ValueError(f"权限 {endpoint_key} 的值必须是布尔类型")