完全移除模块级权限系统,迁移至接口级权限
后端: - 删除 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:
@@ -7,8 +7,8 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status,
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked
|
||||
from app.core.project_permissions import role_has_project_permission
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.core.security import decode_token
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import contract_fee as contract_fee_crud
|
||||
@@ -48,20 +48,6 @@ async def _resolve_project_id(db: AsyncSession, entity_type: str, entity_id: uui
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不支持的附件类型")
|
||||
|
||||
|
||||
async def _ensure_project_access(db: AsyncSession, project_id: uuid.UUID, current_user, write: bool = False):
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value == "ADMIN":
|
||||
return None
|
||||
membership = await member_crud.get_member(db, project_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
action = "write" if write else "read"
|
||||
allowed = await role_has_project_permission(db, project_id, membership.role_in_study, "fees", action)
|
||||
if not allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="项目权限不足")
|
||||
return membership
|
||||
|
||||
|
||||
async def _authorize_download_user(request: Request, db: AsyncSession):
|
||||
token = None
|
||||
auth_header = request.headers.get("Authorization")
|
||||
@@ -82,7 +68,10 @@ async def _authorize_download_user(request: Request, db: AsyncSession):
|
||||
"/attachments",
|
||||
response_model=FeeApiResponse[FeeAttachmentRead],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(get_current_user), Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("fees_attachments:create")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def upload_fee_attachment(
|
||||
entity_type: str = Form(...),
|
||||
@@ -97,7 +86,6 @@ async def upload_fee_attachment(
|
||||
if file_type not in ALLOWED_FILE_TYPES.get(entity_type, set()):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="附件文件类型无效")
|
||||
project_id = await _resolve_project_id(db, entity_type, entity_id)
|
||||
await _ensure_project_access(db, project_id, current_user, write=True)
|
||||
|
||||
dest_dir = UPLOAD_ROOT / f"{entity_type}_{entity_id}"
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -151,7 +139,7 @@ async def upload_fee_attachment(
|
||||
@router.get(
|
||||
"/attachments",
|
||||
response_model=FeeApiResponse[list[FeeAttachmentRead]],
|
||||
dependencies=[Depends(get_current_user)],
|
||||
dependencies=[Depends(require_api_permission("fees_attachments:read"))],
|
||||
)
|
||||
async def list_fee_attachments(
|
||||
entity_type: str,
|
||||
@@ -162,7 +150,6 @@ async def list_fee_attachments(
|
||||
if entity_type not in ALLOWED_ENTITY_TYPES:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="附件类型无效")
|
||||
project_id = await _resolve_project_id(db, entity_type, entity_id)
|
||||
await _ensure_project_access(db, project_id, current_user, write=False)
|
||||
attachments = await fee_attachment_crud.list_attachments(db, entity_type=entity_type, entity_id=entity_id)
|
||||
user_ids = {a.uploaded_by for a in attachments if a.uploaded_by}
|
||||
users_map = await user_crud.get_users_by_ids(db, user_ids)
|
||||
@@ -202,7 +189,16 @@ async def download_fee_attachment(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
|
||||
current_user = await _authorize_download_user(request, db)
|
||||
project_id = await _resolve_project_id(db, attachment.entity_type, attachment.entity_id)
|
||||
await _ensure_project_access(db, project_id, current_user, write=False)
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != "ADMIN":
|
||||
membership = await member_crud.get_member(db, project_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
allowed = await role_has_api_permission(
|
||||
db, project_id, membership.role_in_study, "fees_attachments:read", check_prerequisites=False
|
||||
)
|
||||
if not allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="项目权限不足")
|
||||
if not attachment.storage_key or not os.path.exists(attachment.storage_key):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="服务器未找到文件")
|
||||
return FileResponse(
|
||||
@@ -216,7 +212,10 @@ async def download_fee_attachment(
|
||||
@router.delete(
|
||||
"/attachments/{attachment_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(get_current_user), Depends(require_study_not_locked())],
|
||||
dependencies=[
|
||||
Depends(require_api_permission("fees_attachments:delete")),
|
||||
Depends(require_study_not_locked())
|
||||
],
|
||||
)
|
||||
async def delete_fee_attachment(
|
||||
attachment_id: uuid.UUID,
|
||||
@@ -227,9 +226,14 @@ async def delete_fee_attachment(
|
||||
if not attachment:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
|
||||
project_id = await _resolve_project_id(db, attachment.entity_type, attachment.entity_id)
|
||||
membership = await _ensure_project_access(db, project_id, current_user, write=True)
|
||||
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
membership = None
|
||||
if role_value != "ADMIN":
|
||||
membership = await member_crud.get_member(db, project_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
|
||||
can_delete = (
|
||||
role_value == "ADMIN"
|
||||
or attachment.uploaded_by == current_user.id
|
||||
|
||||
Reference in New Issue
Block a user