移除QA预设角色并收紧角色模板编辑

This commit is contained in:
Cheng Zhou
2026-05-26 11:32:14 +08:00
parent 6c2f61eebd
commit 3c168565ce
26 changed files with 387 additions and 119 deletions
-1
View File
@@ -18,7 +18,6 @@ router = APIRouter()
PROJECT_ROLE_RANK = {
"ADMIN": 100,
"PM": 80,
"QA": 60,
"PV": 50,
"MEDICAL_REVIEW": 50,
"CRA": 40,
+2
View File
@@ -25,6 +25,8 @@ 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:
+31 -31
View File
@@ -20,14 +20,14 @@ API_ENDPOINT_PERMISSIONS = {
"module": "subjects",
"action": "read",
"description": "查询参与者列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"subjects:read": {
"module": "subjects",
"action": "read",
"description": "查询参与者详情",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"subjects:update": {
@@ -56,14 +56,14 @@ API_ENDPOINT_PERMISSIONS = {
"module": "subjects",
"action": "read",
"description": "查询访视列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"visits:read": {
"module": "subjects",
"action": "read",
"description": "查询访视详情",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"visits:update": {
@@ -92,14 +92,14 @@ API_ENDPOINT_PERMISSIONS = {
"module": "risk_issues",
"action": "read",
"description": "查询不良事件列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"risk_issues:read": {
"module": "risk_issues",
"action": "read",
"description": "查询不良事件详情",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"risk_issues:update": {
@@ -251,13 +251,13 @@ API_ENDPOINT_PERMISSIONS = {
"module": "sites",
"action": "read",
"description": "查询中心列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"sites:read": {
"module": "sites",
"action": "read",
"description": "查询中心详情",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"sites:update": {
"module": "sites",
@@ -413,7 +413,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "project_overview",
"action": "read",
"description": "查询项目概览",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
# 监查问题管理
"monitoring_issues:create": {
@@ -470,7 +470,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "monitoring_audit",
"action": "read",
"description": "查询监查访视问题",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"],
"prerequisite_permissions": [],
},
"monitoring_audit:update": {
@@ -499,14 +499,14 @@ API_ENDPOINT_PERMISSIONS = {
"module": "materials",
"action": "read",
"description": "查询药物发货列表",
"default_roles": ["PM", "CRA", "IMP", "QA"],
"default_roles": ["PM", "CRA", "IMP"],
"prerequisite_permissions": [],
},
"drug_shipments:read": {
"module": "materials",
"action": "read",
"description": "查询药物发货详情",
"default_roles": ["PM", "CRA", "IMP", "QA"],
"default_roles": ["PM", "CRA", "IMP"],
"prerequisite_permissions": [],
},
"drug_shipments:update": {
@@ -534,13 +534,13 @@ API_ENDPOINT_PERMISSIONS = {
"module": "materials",
"action": "read",
"description": "查询物资列表",
"default_roles": ["PM", "CRA", "IMP", "QA"],
"default_roles": ["PM", "CRA", "IMP"],
},
"materials:read": {
"module": "materials",
"action": "read",
"description": "查询物资详情",
"default_roles": ["PM", "CRA", "IMP", "QA"],
"default_roles": ["PM", "CRA", "IMP"],
},
"materials:update": {
"module": "materials",
@@ -595,19 +595,19 @@ API_ENDPOINT_PERMISSIONS = {
"module": "audit_export",
"action": "read",
"description": "查询审计日志列表",
"default_roles": ["PM", "QA"],
"default_roles": ["PM"],
},
"audit_logs:read": {
"module": "audit_export",
"action": "read",
"description": "查询审计日志详情",
"default_roles": ["PM", "QA"],
"default_roles": ["PM"],
},
"audit_logs:export": {
"module": "audit_export",
"action": "read",
"description": "导出审计日志",
"default_roles": ["PM", "QA"],
"default_roles": ["PM"],
},
# 知识库笔记管理
"knowledge_notes:create": {
@@ -620,13 +620,13 @@ API_ENDPOINT_PERMISSIONS = {
"module": "shared_library",
"action": "read",
"description": "查询知识库笔记列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"knowledge_notes:read": {
"module": "shared_library",
"action": "read",
"description": "查询知识库笔记详情",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"knowledge_notes:update": {
"module": "shared_library",
@@ -676,7 +676,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "setup_config",
"action": "read",
"description": "查询立项配置",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"setup_config:update": {
"module": "setup_config",
@@ -707,7 +707,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "project_milestones",
"action": "read",
"description": "查询项目里程碑列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"project_milestones:update": {
"module": "project_milestones",
@@ -751,7 +751,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "attachments",
"action": "read",
"description": "查询附件",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"attachments:update": {
"module": "attachments",
@@ -795,7 +795,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "faq",
"action": "read",
"description": "查询FAQ",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"faq:update": {
"module": "faq",
@@ -820,7 +820,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "faq",
"action": "read",
"description": "查询FAQ分类",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"faq_category:update": {
"module": "faq",
@@ -852,7 +852,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "dashboard",
"action": "read",
"description": "查询仪表板",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": ["sites:read", "subjects:read"],
},
# 参与者历史管理
@@ -867,14 +867,14 @@ API_ENDPOINT_PERMISSIONS = {
"module": "subject_histories",
"action": "read",
"description": "查询参与者历史列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"subject_histories:read": {
"module": "subject_histories",
"action": "read",
"description": "查询参与者历史详情",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
"prerequisite_permissions": [],
},
"subject_histories:update": {
@@ -902,13 +902,13 @@ API_ENDPOINT_PERMISSIONS = {
"module": "material_equipments",
"action": "read",
"description": "查询物资设备列表",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"material_equipments:read": {
"module": "material_equipments",
"action": "read",
"description": "查询物资设备详情",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"material_equipments:update": {
"module": "material_equipments",
@@ -933,7 +933,7 @@ API_ENDPOINT_PERMISSIONS = {
"module": "documents",
"action": "read",
"description": "查询文档",
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
},
"documents:update": {
"module": "documents",
@@ -1170,7 +1170,7 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
}
# 项目级权限角色列表
PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]
# 前置权限映射表(用于权限检查)
# 定义每个操作需要的前置权限
+2 -2
View File
@@ -7,7 +7,7 @@ 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
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES, PROJECT_PERMISSION_ROLES
from app.core.permission_cache import get_permission_cache
@@ -114,7 +114,7 @@ async def get_api_endpoint_permissions(
study_result = await db.execute(select(Study).where(Study.id == study_id))
study = study_result.scalar_one_or_none()
active_roles = [role for role in (study.active_roles if study else []) if isinstance(role, str) and role.strip()]
roles = list(dict.fromkeys(["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", *active_roles, *overrides.keys()]))
roles = list(dict.fromkeys([*PROJECT_PERMISSION_ROLES, *active_roles, *overrides.keys()]))
matrix: dict[str, dict[str, dict[str, bool]]] = {}
for role in roles:
matrix[role] = {}
-1
View File
@@ -22,7 +22,6 @@ ROLE_ACTIONS: dict[str, set[str]] = {
"ack",
"delete_document",
},
"QA": {"view", "create_document", "create_version", "submit", "approve", "distribute", "ack"},
"PM": {"view", "create_document", "create_version", "submit", "approve", "distribute", "ack", "delete_document"},
"CRA": {"view", "create_version", "submit", "ack"},
"PV": {"view", "create_version", "submit", "ack"},
-1
View File
@@ -19,7 +19,6 @@ class UserRole(str, enum.Enum):
PV = "PV"
MEDICAL_REVIEW = "MEDICAL_REVIEW"
IMP = "IMP"
QA = "QA"
class UserStatus(str, enum.Enum):
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Literal
from pydantic import BaseModel, Field
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]
ProjectPermissionAction = Literal["read", "write"]
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", "ADMIN"]
UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "ADMIN"]
UserStatus = Literal["PENDING", "ACTIVE", "REJECTED", "DISABLED"]
PASSWORD_REGEX = re.compile(r"^(?=.*[A-Za-z])(?=.*\d).{8,}$")
@@ -7,7 +7,7 @@ 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.core.api_permissions import API_ENDPOINT_PERMISSIONS, PROJECT_PERMISSION_ROLES
from app.schemas.permission_template import PermissionTemplateCreate, PermissionTemplateUpdate
@@ -21,6 +21,9 @@ class PermissionTemplateService:
created_by: UUID,
) -> PermissionTemplate:
"""创建权限模板"""
if payload.template_type != TemplateType.CUSTOM:
raise ValueError("只能新增自定义角色")
# 验证权限配置
await PermissionTemplateService._validate_permissions(payload.permissions)
@@ -78,6 +81,11 @@ class PermissionTemplateService:
query = query.where(PermissionTemplate.template_type == template_type)
if category:
query = query.where(PermissionTemplate.category == category)
elif template_type == TemplateType.ROLE:
query = query.where(
(PermissionTemplate.is_system.is_(False))
| (PermissionTemplate.category.in_(PROJECT_PERMISSION_ROLES))
)
query = query.offset(skip).limit(limit)
result = await db.execute(query)
@@ -94,9 +102,11 @@ class PermissionTemplateService:
if not template:
raise ValueError(f"模板 {template_id} 不存在")
# 不允许修改系统预设模板
# 系统预设模板只允许更新展示元数据,权限配置和分类等结构字段保持受保护。
if template.is_system:
raise ValueError("不允许修改系统预设模板")
protected_fields = {"permissions", "tags", "category", "recommended_roles"}
if protected_fields.intersection(payload.model_fields_set):
raise ValueError("不允许修改系统预设模板")
# 验证权限配置
if payload.permissions: