统一项目权限读取模型

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
+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(