权限管理:新增医学审核项目角色
新增 MEDICAL_REVIEW 项目角色,界面显示为医学审核,并通过 Alembic 迁移扩展 user_role 枚举。 为医学审核配置独立默认权限:项目总览、参与者、风险问题、监查稽查、FAQ、共享库按医学审核职责开放,不再复制 PV 默认权限。 同步前后端类型、项目成员角色下拉、权限矩阵展示、文件分发角色兜底和用户协议文案,并补充权限矩阵回归测试。
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""add medical review role
|
||||
|
||||
Revision ID: 20260513_01
|
||||
Revises: 20260512_03
|
||||
Create Date: 2026-05-13 09:15:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "20260513_01"
|
||||
down_revision: Union[str, None] = "20260512_03"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("ALTER TYPE user_role ADD VALUE IF NOT EXISTS 'MEDICAL_REVIEW'")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.study_role_permission import StudyRolePermission
|
||||
|
||||
PROJECT_PERMISSION_ROLES = ("ADMIN", "PM", "CRA", "PV", "IMP", "QA")
|
||||
PROJECT_PERMISSION_ROLES = ("ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA")
|
||||
|
||||
PROJECT_PERMISSION_MODULES = (
|
||||
{"key": "project_members", "label": "项目成员", "description": "维护项目账号、成员角色与启停状态"},
|
||||
@@ -93,6 +93,24 @@ DEFAULT_PROJECT_ROLE_PERMISSIONS: dict[str, dict[str, dict[str, bool]]] = {
|
||||
"sites": {"read": False, "write": False},
|
||||
"audit_export": {"read": False, "write": False},
|
||||
},
|
||||
"MEDICAL_REVIEW": {
|
||||
"project_overview": {"read": True, "write": False},
|
||||
"project_milestones": {"read": False, "write": False},
|
||||
"fees": {"read": False, "write": False},
|
||||
"materials": {"read": False, "write": False},
|
||||
"file_versions": {"read": False, "write": False},
|
||||
"startup_ethics": {"read": False, "write": False},
|
||||
"startup_auth": {"read": False, "write": False},
|
||||
"subjects": {"read": True, "write": False},
|
||||
"risk_issues": {"read": True, "write": True},
|
||||
"monitoring_audit": {"read": True, "write": False},
|
||||
"etmf": {"read": False, "write": False},
|
||||
"faq": {"read": True, "write": True},
|
||||
"shared_library": {"read": True, "write": True},
|
||||
"project_members": {"read": False, "write": False},
|
||||
"sites": {"read": False, "write": False},
|
||||
"audit_export": {"read": False, "write": False},
|
||||
},
|
||||
"IMP": {
|
||||
"project_overview": {"read": True, "write": False},
|
||||
"project_milestones": {"read": True, "write": False},
|
||||
|
||||
@@ -14,6 +14,7 @@ class UserRole(str, enum.Enum):
|
||||
PM = "PM"
|
||||
CRA = "CRA"
|
||||
PV = "PV"
|
||||
MEDICAL_REVIEW = "MEDICAL_REVIEW"
|
||||
IMP = "IMP"
|
||||
QA = "QA"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Literal, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from app.schemas.user import UserDisplay
|
||||
|
||||
StudyRole = Literal["PM", "CRA", "PV", "IMP", "QA", "ADMIN"]
|
||||
StudyRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", "ADMIN"]
|
||||
|
||||
|
||||
class StudyMemberCreate(BaseModel):
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "IMP", "QA"]
|
||||
ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
|
||||
ProjectPermissionAction = Literal["read", "write"]
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
UserRole = Literal["PM", "CRA", "PV", "IMP", "QA", "ADMIN"]
|
||||
RegisterRole = Literal["PM", "CRA", "PV", "IMP"]
|
||||
UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", "ADMIN"]
|
||||
RegisterRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]
|
||||
UserStatus = Literal["PENDING", "ACTIVE", "REJECTED", "DISABLED"]
|
||||
|
||||
PASSWORD_REGEX = re.compile(r"^(?=.*[A-Za-z])(?=.*\d).{8,}$")
|
||||
|
||||
@@ -75,6 +75,14 @@ def test_default_business_permissions_match_role_responsibilities():
|
||||
assert matrix["CRA"]["subjects"] == {"read": True, "write": True}
|
||||
assert matrix["CRA"]["monitoring_audit"] == {"read": True, "write": True}
|
||||
assert matrix["PV"]["risk_issues"] == {"read": True, "write": True}
|
||||
assert matrix["MEDICAL_REVIEW"]["project_overview"] == {"read": True, "write": False}
|
||||
assert matrix["MEDICAL_REVIEW"]["subjects"] == {"read": True, "write": False}
|
||||
assert matrix["MEDICAL_REVIEW"]["risk_issues"] == {"read": True, "write": True}
|
||||
assert matrix["MEDICAL_REVIEW"]["monitoring_audit"] == {"read": True, "write": False}
|
||||
assert matrix["MEDICAL_REVIEW"]["faq"] == {"read": True, "write": True}
|
||||
assert matrix["MEDICAL_REVIEW"]["shared_library"] == {"read": True, "write": True}
|
||||
assert matrix["MEDICAL_REVIEW"]["fees"] == {"read": False, "write": False}
|
||||
assert matrix["MEDICAL_REVIEW"]["materials"] == {"read": False, "write": False}
|
||||
assert matrix["IMP"]["materials"] == {"read": True, "write": True}
|
||||
assert matrix["PM"]["faq"] == {"read": True, "write": True}
|
||||
assert matrix["CRA"]["faq"] == {"read": True, "write": True}
|
||||
@@ -115,16 +123,16 @@ def test_non_pm_roles_cannot_be_granted_management_backend_permissions():
|
||||
module: {"read": True, "write": True}
|
||||
for module in management_modules
|
||||
}
|
||||
for role in ["CRA", "PV", "IMP", "QA"]
|
||||
for role in ["CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
|
||||
})
|
||||
|
||||
for role in ["CRA", "PV", "IMP", "QA"]:
|
||||
for role in ["CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]:
|
||||
for module in management_modules:
|
||||
assert matrix[role][module] == {"read": False, "write": False}
|
||||
|
||||
|
||||
def test_each_non_admin_role_can_toggle_each_writable_module():
|
||||
roles = ["PM", "CRA", "PV", "IMP", "QA"]
|
||||
roles = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
|
||||
management_modules = {"project_members", "sites", "audit_export"}
|
||||
writable_modules = [
|
||||
module["key"]
|
||||
@@ -162,11 +170,20 @@ def test_read_only_modules_never_accept_write_for_any_role():
|
||||
module: {"read": False, "write": True}
|
||||
for module in read_only_modules
|
||||
}
|
||||
for role in ["ADMIN", "PM", "CRA", "PV", "IMP", "QA"]
|
||||
for role in ["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
|
||||
}
|
||||
|
||||
matrix = normalize_permission_matrix(payload)
|
||||
|
||||
for role in ["ADMIN", "PM", "CRA", "PV", "IMP", "QA"]:
|
||||
for role in ["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]:
|
||||
for module in read_only_modules:
|
||||
assert matrix[role][module] == {"read": True, "write": False}
|
||||
|
||||
|
||||
def test_medical_review_default_permissions_are_independent_from_pv():
|
||||
matrix = normalize_permission_matrix()
|
||||
|
||||
assert matrix["PV"]["monitoring_audit"] == {"read": True, "write": True}
|
||||
assert matrix["MEDICAL_REVIEW"]["monitoring_audit"] == {"read": True, "write": False}
|
||||
assert matrix["PV"]["fees"] == {"read": True, "write": False}
|
||||
assert matrix["MEDICAL_REVIEW"]["fees"] == {"read": False, "write": False}
|
||||
|
||||
Reference in New Issue
Block a user