统一项目权限读取模型

1、合并列表与详情读取权限,统一使用 subjects:read、project_members:read、monitoring_issues:read 等业务读取 key。

2、增加历史读取权限 key 的兼容映射,确保存量项目权限覆盖值不会丢失。

3、同步前端权限工具、路由权限与权限管理页面,避免继续引用已合并的旧权限 key。

4、补充项目角色启停保护与抽屉未保存变更守卫,防止停用 PM 或仍有关联成员的角色。
This commit is contained in:
Cheng Zhou
2026-06-04 11:06:03 +08:00
parent 44d69c2d7b
commit ab4f0d93ed
22 changed files with 852 additions and 349 deletions
+2 -2
View File
@@ -122,7 +122,7 @@ async def _list_ae_records(
@router.get(
"/summary",
response_model=list[AERead],
dependencies=[Depends(require_api_permission("subject_aes:list"))],
dependencies=[Depends(require_api_permission("subject_aes:read"))],
)
async def list_risk_issue_ae(
study_id: uuid.UUID,
@@ -149,7 +149,7 @@ async def list_risk_issue_ae(
@router.get(
"/",
response_model=list[AERead],
dependencies=[Depends(require_api_permission("subject_aes:list"))],
dependencies=[Depends(require_api_permission("subject_aes:read"))],
)
async def list_ae(
study_id: uuid.UUID,
+4 -4
View File
@@ -142,10 +142,10 @@ async def add_member(
@router.get(
"/",
response_model=list[StudyMemberReadWithUser],
dependencies=[Depends(require_api_permission("project_members:list"))],
dependencies=[Depends(require_api_permission("project_members:read"))],
)
@register_api_endpoint(
endpoint_key="project_members:list",
endpoint_key="project_members:read",
module="project_members",
action="read",
description="查询项目成员列表",
@@ -188,10 +188,10 @@ async def list_members(
@router.get(
"/candidates",
response_model=list[UserResponse],
dependencies=[Depends(require_api_permission("project_members:candidates"))],
dependencies=[Depends(require_api_permission("project_members:read"))],
)
@register_api_endpoint(
endpoint_key="project_members:candidates",
endpoint_key="project_members:read",
module="project_members",
action="read",
description="查询项目成员候选人",
@@ -332,7 +332,7 @@ def _normalize_file_rows(filename: str, content: bytes) -> list[dict[str, object
@router.get(
"/issues",
response_model=list[MonitoringVisitIssueRead],
dependencies=[Depends(require_api_permission("monitoring_issues:list"))],
dependencies=[Depends(require_api_permission("monitoring_issues:read"))],
)
async def list_monitoring_visit_issues(
study_id: uuid.UUID,
@@ -421,7 +421,7 @@ async def create_monitoring_visit_issue(
@router.get(
"/issues/export",
response_class=StreamingResponse,
dependencies=[Depends(require_api_permission("monitoring_issues:list"))],
dependencies=[Depends(require_api_permission("monitoring_issues:read"))],
)
async def export_monitoring_visit_issues(
study_id: uuid.UUID,
+41 -6
View File
@@ -9,13 +9,14 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_db_session, require_study_roles
from app.core.deps import get_db_session, is_system_admin, require_study_roles
from app.models.study import Study
from app.models.study_member import StudyMember
router = APIRouter(prefix="/active-roles", tags=["active-roles"])
def _normalize_active_roles(value: object) -> list[str]:
def _normalize_active_roles(value: object, *, preserve_pm: bool = False) -> list[str]:
if not isinstance(value, list):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="active_roles 必须是数组")
roles: list[str] = []
@@ -25,15 +26,44 @@ def _normalize_active_roles(value: object) -> list[str]:
continue
if role == "ADMIN":
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="ADMIN 不能作为项目角色")
if role == "QA":
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="QA 不能作为项目角色")
if len(role) > 20:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="项目角色长度不能超过20个字符")
if role not in roles:
roles.append(role)
if preserve_pm and "PM" not in roles:
roles.insert(0, "PM")
return roles
async def _ensure_removed_roles_unused(
db: AsyncSession,
study_id: uuid.UUID,
current_roles: object,
next_roles: list[str],
) -> None:
if not isinstance(current_roles, list):
return
removed_roles = [role for role in current_roles if isinstance(role, str) and role not in next_roles]
if not removed_roles:
return
result = await db.execute(
select(StudyMember.role_in_study)
.where(
StudyMember.study_id == study_id,
StudyMember.is_active.is_(True),
StudyMember.role_in_study.in_(removed_roles),
)
.order_by(StudyMember.role_in_study)
)
role_in_use = result.scalars().first()
if role_in_use:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"角色 {role_in_use} 仍有成员使用,不能停用",
)
@router.get("", summary="获取项目已生效角色列表")
async def get_active_roles(
study_id: uuid.UUID,
@@ -51,13 +81,18 @@ async def get_active_roles(
async def update_active_roles(
study_id: uuid.UUID,
payload: dict,
_=Depends(require_study_roles(["PM"])),
current_user=Depends(require_study_roles(["PM"])),
db: Annotated[AsyncSession, Depends(get_db_session)] = None,
) -> dict:
result = await db.execute(select(Study).where(Study.id == study_id))
study = result.scalar_one_or_none()
if not study:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
study.active_roles = _normalize_active_roles(payload.get("active_roles", []))
active_roles = _normalize_active_roles(
payload.get("active_roles", []),
preserve_pm=not is_system_admin(current_user),
)
await _ensure_removed_roles_unused(db, study_id, study.active_roles, active_roles)
study.active_roles = active_roles
await db.commit()
return {"active_roles": study.active_roles}
+1 -1
View File
@@ -66,7 +66,7 @@ async def create_history(
@router.get(
"/histories",
response_model=list[SubjectHistoryRead],
dependencies=[Depends(require_api_permission("subject_histories:list"))],
dependencies=[Depends(require_api_permission("subject_histories:read"))],
)
async def list_histories(
study_id: uuid.UUID,
+1 -1
View File
@@ -66,7 +66,7 @@ async def create_subject(
@router.get(
"/",
response_model=list[SubjectRead],
dependencies=[Depends(require_api_permission("subjects:list"))],
dependencies=[Depends(require_api_permission("subjects:read"))],
)
async def list_subjects(
study_id: uuid.UUID,
+47 -212
View File
@@ -5,6 +5,28 @@
from __future__ import annotations
READ_PERMISSION_ALIASES: dict[str, str] = {
"subjects:list": "subjects:read",
"subject_aes:list": "subject_aes:read",
"fees_contracts:list": "fees_contracts:read",
"project_members:list": "project_members:read",
"project_members:candidates": "project_members:read",
"sites:list": "sites:read",
"startup_ethics:list": "startup_ethics:read",
"startup_initiation:list": "startup_initiation:read",
"monitoring_issues:list": "monitoring_issues:read",
"drug_shipments:list": "drug_shipments:read",
"audit_logs:list": "audit_logs:read",
"precautions:list": "precautions:read",
"subject_histories:list": "subject_histories:read",
"material_equipments:list": "material_equipments:read",
}
def canonical_permission_key(endpoint_key: str) -> str:
return READ_PERMISSION_ALIASES.get(endpoint_key, endpoint_key)
# 接口级权限配置
# 格式: "业务操作名": {配置信息}
API_ENDPOINT_PERMISSIONS = {
@@ -16,17 +38,10 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM", "CRA"],
"prerequisite_permissions": ["sites:read"],
},
"subjects:list": {
"module": "subjects",
"action": "read",
"description": "查询参与者列表",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
"prerequisite_permissions": [],
},
"subjects:read": {
"module": "subjects",
"action": "read",
"description": "查询参与者详情",
"description": "查询参与者",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
"prerequisite_permissions": [],
},
@@ -81,17 +96,10 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM", "CRA", "PV", "QA"],
"prerequisite_permissions": ["subjects:read", "sites:read"],
},
"subject_aes:list": {
"module": "subjects",
"action": "read",
"description": "查询参与者AE列表",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
"prerequisite_permissions": [],
},
"subject_aes:read": {
"module": "subjects",
"action": "read",
"description": "查询参与者AE详情",
"description": "查询参与者AE",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
"prerequisite_permissions": [],
},
@@ -100,14 +108,14 @@ API_ENDPOINT_PERMISSIONS = {
"action": "write",
"description": "更新参与者AE",
"default_roles": ["PM", "CRA", "PV", "QA"],
"prerequisite_permissions": ["subject_aes:list", "subjects:read", "sites:read"],
"prerequisite_permissions": ["subject_aes:read", "subjects:read", "sites:read"],
},
"subject_aes:delete": {
"module": "subjects",
"action": "write",
"description": "删除参与者AE",
"default_roles": ["PM"],
"prerequisite_permissions": ["subject_aes:list", "subjects:read", "sites:read"],
"prerequisite_permissions": ["subject_aes:read", "subjects:read", "sites:read"],
},
# 合同费用管理 (fees_contracts)
"fees_contracts:create": {
@@ -117,17 +125,10 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM"],
"prerequisite_permissions": ["sites:read"],
},
"fees_contracts:list": {
"module": "fees",
"action": "read",
"description": "查询合同费用列表",
"default_roles": ["PM", "CRA", "CTA"],
"prerequisite_permissions": [],
},
"fees_contracts:read": {
"module": "fees",
"action": "read",
"description": "查询合同费用详情",
"description": "查询合同费用",
"default_roles": ["PM", "CRA", "CTA"],
"prerequisite_permissions": [],
},
@@ -152,16 +153,10 @@ API_ENDPOINT_PERMISSIONS = {
"description": "添加项目成员",
"default_roles": ["PM"],
},
"project_members:list": {
"project_members:read": {
"module": "project_members",
"action": "read",
"description": "查询项目成员列表",
"default_roles": ["PM"],
},
"project_members:candidates": {
"module": "project_members",
"action": "read",
"description": "查询项目成员候选人",
"description": "查询项目成员",
"default_roles": ["PM"],
},
"project_members:update": {
@@ -183,16 +178,10 @@ API_ENDPOINT_PERMISSIONS = {
"description": "创建中心",
"default_roles": ["PM"],
},
"sites:list": {
"module": "sites",
"action": "read",
"description": "查询中心列表",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
},
"sites:read": {
"module": "sites",
"action": "read",
"description": "查询中心详情",
"description": "查询中心",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
},
"sites:update": {
@@ -214,16 +203,10 @@ API_ENDPOINT_PERMISSIONS = {
"description": "创建伦理记录",
"default_roles": ["PM"],
},
"startup_ethics:list": {
"module": "startup_ethics",
"action": "read",
"description": "查询伦理记录列表",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"startup_ethics:read": {
"module": "startup_ethics",
"action": "read",
"description": "查询伦理记录详情",
"description": "查询伦理记录",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"startup_ethics:update": {
@@ -245,16 +228,10 @@ API_ENDPOINT_PERMISSIONS = {
"description": "创建立项记录",
"default_roles": ["PM"],
},
"startup_initiation:list": {
"module": "startup_ethics",
"action": "read",
"description": "查询立项记录列表",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"startup_initiation:read": {
"module": "startup_ethics",
"action": "read",
"description": "查询立项记录详情",
"description": "查询立项记录",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"startup_initiation:update": {
@@ -278,16 +255,10 @@ API_ENDPOINT_PERMISSIONS = {
"prerequisite_permissions": ["sites:read", "subjects:read"],
},
# 监查访视问题
"monitoring_issues:list": {
"module": "risk_issues",
"action": "read",
"description": "查询监查访视问题列表",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"monitoring_issues:read": {
"module": "risk_issues",
"action": "read",
"description": "查询监查访视问题详情",
"description": "查询监查访视问题",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"monitoring_issues:create": {
@@ -319,17 +290,10 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM", "CTA"],
"prerequisite_permissions": ["sites:read"],
},
"drug_shipments:list": {
"module": "materials",
"action": "read",
"description": "查询药物发货列表",
"default_roles": ["PM", "CRA", "CTA"],
"prerequisite_permissions": [],
},
"drug_shipments:read": {
"module": "materials",
"action": "read",
"description": "查询药物发货详情",
"description": "查询药物发货",
"default_roles": ["PM", "CRA", "CTA"],
"prerequisite_permissions": [],
},
@@ -377,16 +341,10 @@ API_ENDPOINT_PERMISSIONS = {
"prerequisite_permissions": ["subjects:read", "sites:read"],
},
# 审计日志管理
"audit_logs:list": {
"module": "audit_export",
"action": "read",
"description": "查询审计日志列表",
"default_roles": ["PM"],
},
"audit_logs:read": {
"module": "audit_export",
"action": "read",
"description": "查询审计日志详情",
"description": "查询审计日志",
"default_roles": ["PM"],
},
"audit_logs:export": {
@@ -402,16 +360,10 @@ API_ENDPOINT_PERMISSIONS = {
"description": "创建注意事项",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"precautions:list": {
"module": "shared_library",
"action": "read",
"description": "查询注意事项列表",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
},
"precautions:read": {
"module": "shared_library",
"action": "read",
"description": "查询注意事项详情",
"description": "查询注意事项",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
},
"precautions:update": {
@@ -453,7 +405,7 @@ API_ENDPOINT_PERMISSIONS = {
},
"setup_config:delete_version": {
"module": "setup_config",
"action": "write",
"action": "delete",
"description": "删除立项配置版本",
"default_roles": ["PM"],
},
@@ -496,107 +448,41 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM"],
},
# 模块附件管理
"fees_contracts_attachments:create": {
"module": "fees",
"action": "write",
"description": "上传合同费用附件",
"default_roles": ["PM"],
},
"fees_contracts_attachments:read": {
"module": "fees",
"action": "read",
"description": "查询合同费用附件",
"default_roles": ["PM", "CRA", "CTA"],
},
"fees_contracts_attachments:delete": {
"module": "fees",
"action": "write",
"description": "删除合同费用附件",
"default_roles": ["PM"],
},
"startup_initiation_attachments:create": {
"module": "startup_ethics",
"action": "write",
"description": "上传立项记录附件",
"default_roles": ["PM"],
},
"startup_initiation_attachments:read": {
"module": "startup_ethics",
"action": "read",
"description": "查询立项记录附件",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"startup_initiation_attachments:delete": {
"module": "startup_ethics",
"action": "write",
"description": "删除立项记录附件",
"default_roles": ["PM"],
},
"startup_ethics_attachments:create": {
"module": "startup_ethics",
"action": "write",
"description": "上传伦理记录附件",
"default_roles": ["PM"],
},
"startup_ethics_attachments:read": {
"module": "startup_ethics",
"action": "read",
"description": "查询伦理记录附件",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"startup_ethics_attachments:delete": {
"module": "startup_ethics",
"action": "write",
"description": "删除伦理记录附件",
"default_roles": ["PM"],
},
"startup_auth_attachments:create": {
"module": "startup_auth",
"action": "write",
"description": "上传启动授权附件",
"default_roles": ["PM"],
},
"startup_auth_attachments:read": {
"module": "startup_auth",
"action": "read",
"description": "查询启动授权附件",
"default_roles": ["PM", "CRA", "PV", "CTA"],
},
"startup_auth_attachments:delete": {
"module": "startup_auth",
"action": "write",
"description": "删除启动授权附件",
"default_roles": ["PM"],
},
"drug_shipments_attachments:create": {
"module": "materials",
"action": "write",
"description": "上传药物发货附件",
"default_roles": ["PM", "CTA"],
},
"drug_shipments_attachments:read": {
"module": "materials",
"action": "read",
"description": "查询药物发货附件",
"default_roles": ["PM", "CRA", "CTA"],
},
"drug_shipments_attachments:delete": {
"module": "materials",
"action": "write",
"description": "删除药物发货附件",
"default_roles": ["PM"],
},
"precautions_attachments:create": {
"module": "shared_library",
"material_equipments_attachments:delete": {
"module": "materials",
"action": "write",
"description": "上传注意事项附件",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"precautions_attachments:read": {
"module": "shared_library",
"action": "read",
"description": "查询注意事项附件",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
"description": "删除物资设备附件",
"default_roles": ["PM"],
},
"precautions_attachments:delete": {
"module": "shared_library",
@@ -604,18 +490,6 @@ API_ENDPOINT_PERMISSIONS = {
"description": "删除注意事项附件",
"default_roles": ["PM"],
},
"faq_attachments:create": {
"module": "shared_library",
"action": "write",
"description": "上传FAQ附件",
"default_roles": ["PM", "CRA", "PV", "QA"],
},
"faq_attachments:read": {
"module": "shared_library",
"action": "read",
"description": "查询FAQ附件",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
},
"faq_attachments:delete": {
"module": "shared_library",
"action": "write",
@@ -693,17 +567,10 @@ API_ENDPOINT_PERMISSIONS = {
"default_roles": ["PM", "CRA"],
"prerequisite_permissions": ["subjects:read"],
},
"subject_histories:list": {
"module": "subjects",
"action": "read",
"description": "查询病史记录列表",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
"prerequisite_permissions": [],
},
"subject_histories:read": {
"module": "subjects",
"action": "read",
"description": "查询病史记录详情",
"description": "查询病史记录",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
"prerequisite_permissions": [],
},
@@ -728,16 +595,10 @@ API_ENDPOINT_PERMISSIONS = {
"description": "创建物资设备",
"default_roles": ["PM", "CRA"],
},
"material_equipments:list": {
"module": "materials",
"action": "read",
"description": "查询物资设备列表",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
},
"material_equipments:read": {
"module": "materials",
"action": "read",
"description": "查询物资设备详情",
"description": "查询物资设备",
"default_roles": ["PM", "CRA", "PV", "QA", "CTA"],
},
"material_equipments:update": {
@@ -784,9 +645,7 @@ API_ENDPOINT_PERMISSIONS = {
OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
"subjects": {
"read": [
"subjects:list",
"subjects:read",
"subject_aes:list",
"subject_aes:read",
],
"write": [
@@ -810,7 +669,6 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"risk_issues": {
"read": [
"monitoring_issues:list",
"monitoring_issues:read",
],
"write": [
@@ -821,22 +679,18 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"fees": {
"read": [
"fees_contracts:list",
"fees_contracts:read",
"fees_contracts_attachments:read",
],
"write": [
"fees_contracts:create",
"fees_contracts:update",
"fees_contracts:delete",
"fees_contracts_attachments:create",
"fees_contracts_attachments:delete",
],
},
"project_members": {
"read": [
"project_members:list",
"project_members:candidates",
"project_members:read",
],
"write": [
"project_members:create",
@@ -846,7 +700,6 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"sites": {
"read": [
"sites:list",
"sites:read",
],
"write": [
@@ -857,12 +710,8 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"startup_ethics": {
"read": [
"startup_ethics:list",
"startup_ethics:read",
"startup_initiation:list",
"startup_initiation:read",
"startup_ethics_attachments:read",
"startup_initiation_attachments:read",
],
"write": [
"startup_ethics:create",
@@ -871,22 +720,18 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
"startup_initiation:create",
"startup_initiation:update",
"startup_initiation:delete",
"startup_ethics_attachments:create",
"startup_ethics_attachments:delete",
"startup_initiation_attachments:create",
"startup_initiation_attachments:delete",
],
},
"startup_auth": {
"read": [
"startup_auth:read",
"startup_auth_attachments:read",
],
"write": [
"startup_auth:create",
"startup_auth:update",
"startup_auth:delete",
"startup_auth_attachments:create",
"startup_auth_attachments:delete",
],
},
@@ -898,11 +743,8 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"materials": {
"read": [
"drug_shipments:list",
"drug_shipments:read",
"material_equipments:list",
"material_equipments:read",
"drug_shipments_attachments:read",
],
"write": [
"drug_shipments:create",
@@ -911,13 +753,12 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
"material_equipments:create",
"material_equipments:update",
"material_equipments:delete",
"drug_shipments_attachments:create",
"drug_shipments_attachments:delete",
"material_equipments_attachments:delete",
],
},
"audit_export": {
"read": [
"audit_logs:list",
"audit_logs:read",
"audit_logs:export",
],
@@ -925,7 +766,6 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"subject_histories": {
"read": [
"subject_histories:list",
"subject_histories:read",
],
"write": [
@@ -965,18 +805,14 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
},
"shared_library": {
"read": [
"precautions:list",
"precautions:read",
"precautions_attachments:read",
"faq:read",
"faq_category:read",
"faq_attachments:read",
],
"write": [
"precautions:create",
"precautions:update",
"precautions:delete",
"precautions_attachments:create",
"precautions_attachments:delete",
"faq:create",
"faq:update",
@@ -986,7 +822,6 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
"faq_category:delete",
"faq_reply:create",
"faq_reply:delete",
"faq_attachments:create",
"faq_attachments:delete",
],
},
@@ -1010,8 +845,8 @@ OPERATION_PREREQUISITES: dict[str, list[str]] = {
# 参与者AE
"subject_aes:create": ["subjects:read", "sites:read"],
"subject_aes:update": ["subject_aes:list", "subjects:read", "sites:read"],
"subject_aes:delete": ["subject_aes:list", "subjects:read", "sites:read"],
"subject_aes:update": ["subject_aes:read", "subjects:read", "sites:read"],
"subject_aes:delete": ["subject_aes:read", "subjects:read", "sites:read"],
# 合同费用
"fees_contracts:create": ["sites:read"],
+14 -2
View File
@@ -7,7 +7,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.models.api_endpoint_permission import ApiEndpointPermission
from app.models.study import Study
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES, PROJECT_PERMISSION_ROLES
from app.core.api_permissions import (
API_ENDPOINT_PERMISSIONS,
OPERATION_PREREQUISITES,
PROJECT_PERMISSION_ROLES,
canonical_permission_key,
)
from app.core.permission_cache import get_permission_cache
@@ -34,7 +39,10 @@ async def _get_project_permission_overrides(
permissions: dict[str, dict[str, bool]] = {}
for row in rows:
permissions.setdefault(row.role, {})[row.endpoint_key] = row.allowed
endpoint_key = canonical_permission_key(row.endpoint_key)
role_permissions = permissions.setdefault(row.role, {})
if row.endpoint_key == endpoint_key or endpoint_key not in role_permissions:
role_permissions[endpoint_key] = row.allowed
cache.set_project_permissions(study_id, permissions)
return permissions
@@ -48,6 +56,7 @@ async def role_has_api_permission(
check_prerequisites: bool = True,
) -> bool:
"""检查角色是否有权访问特定接口"""
endpoint_key = canonical_permission_key(endpoint_key)
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
return False
@@ -93,6 +102,7 @@ async def get_missing_prerequisites(
return []
missing = []
endpoint_key = canonical_permission_key(endpoint_key)
prerequisites = OPERATION_PREREQUISITES.get(endpoint_key, [])
for prereq_endpoint in prerequisites:
has_prereq = await role_has_api_permission(
@@ -132,6 +142,7 @@ async def get_api_endpoint_permissions(
if role not in matrix:
matrix[role] = {}
for endpoint_key, allowed in endpoints.items():
endpoint_key = canonical_permission_key(endpoint_key)
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
continue
matrix[role][endpoint_key] = {"allowed": allowed}
@@ -153,6 +164,7 @@ async def replace_api_endpoint_permissions(
if role in ("ADMIN", "PM"):
continue
for endpoint_key, allowed in endpoints.items():
endpoint_key = canonical_permission_key(endpoint_key)
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
continue
result = await db.execute(