完全移除模块级权限系统,迁移至接口级权限
后端: - 删除 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:
@@ -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} 的值必须是布尔类型")
|
||||
Reference in New Issue
Block a user