整理项目文档并优化权限监控与安装脚本

This commit is contained in:
Cheng Zhou
2026-05-21 11:39:00 +08:00
parent 6d682103f3
commit 9305ced664
31 changed files with 508 additions and 4083 deletions
+452
View File
@@ -0,0 +1,452 @@
# 权限系统测试指南
## 测试环境准备
### 1. 数据库迁移
```bash
cd backend
alembic upgrade head
```
### 2. 启动应用
```bash
uvicorn app.main:app --reload
```
### 3. 验证API端点注册
访问 `http://localhost:8000/api/v1/api-permissions/endpoints` 查看所有已注册的API端点。
## 单元测试
### 1. 权限检查函数测试
**文件**: `backend/tests/test_api_permissions.py`
```python
import pytest
from app.core.project_permissions import role_has_api_permission
from app.models.api_endpoint_permission import ApiEndpointPermission
@pytest.mark.asyncio
async def test_api_permission_check_allowed(db_session, study_id):
"""测试接口级权限检查 - 允许"""
# 创建权限记录
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="POST:/subjects",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "POST:/subjects"
)
assert result is True
@pytest.mark.asyncio
async def test_api_permission_check_denied(db_session, study_id):
"""测试接口级权限检查 - 拒绝"""
# 创建权限记录
perm = ApiEndpointPermission(
study_id=study_id,
role="PV",
endpoint_key="POST:/subjects",
allowed=False,
)
db_session.add(perm)
await db_session.commit()
# 验证权限
result = await role_has_api_permission(
db_session, study_id, "PV", "POST:/subjects"
)
assert result is False
@pytest.mark.asyncio
async def test_api_permission_fallback_to_module_level(db_session, study_id):
"""测试权限回退 - 接口级权限未配置时回退到模块级权限"""
# 不创建接口级权限,应该回退到模块级权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "POST:/subjects"
)
# 结果取决于模块级权限配置
assert isinstance(result, bool)
@pytest.mark.asyncio
async def test_admin_always_allowed(db_session, study_id):
"""测试ADMIN角色总是被允许"""
result = await role_has_api_permission(
db_session, study_id, "ADMIN", "POST:/subjects"
)
assert result is True
```
### 2. 权限配置测试
**文件**: `backend/tests/test_api_permissions_config.py`
```python
import pytest
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, MODULE_TO_ENDPOINTS
def test_api_endpoint_permissions_structure():
"""测试API端点权限配置结构"""
for endpoint_key, config in API_ENDPOINT_PERMISSIONS.items():
assert "module" in config
assert "action" in config
assert "description" in config
assert "default_roles" in config
assert config["action"] in ["read", "write"]
assert isinstance(config["default_roles"], list)
def test_module_to_endpoints_mapping():
"""测试模块到端点的映射"""
for module, actions in MODULE_TO_ENDPOINTS.items():
assert "read" in actions
assert "write" in actions
assert isinstance(actions["read"], list)
assert isinstance(actions["write"], list)
# 验证所有端点都在API_ENDPOINT_PERMISSIONS中定义
for endpoint_key in actions["read"] + actions["write"]:
assert endpoint_key in API_ENDPOINT_PERMISSIONS
def test_subjects_endpoints_configured():
"""测试subjects模块的端点配置"""
expected_endpoints = [
"POST:/subjects",
"GET:/subjects",
"GET:/subjects/{id}",
"PATCH:/subjects/{id}",
"DELETE:/subjects/{id}",
]
for endpoint_key in expected_endpoints:
assert endpoint_key in API_ENDPOINT_PERMISSIONS
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "subjects"
def test_fees_endpoints_configured():
"""测试fees模块的端点配置"""
expected_endpoints = [
"POST:/fees/contracts",
"GET:/fees/contracts",
"GET:/fees/contracts/{id}",
"PATCH:/fees/contracts/{id}",
"DELETE:/fees/contracts/{id}",
"POST:/fees/contracts/{id}/payments",
"PATCH:/fees/payments/{id}",
"DELETE:/fees/payments/{id}",
"POST:/finance/contracts",
"GET:/finance/contracts",
"GET:/finance/contracts/{id}",
"PATCH:/finance/contracts/{id}",
"DELETE:/finance/contracts/{id}",
]
for endpoint_key in expected_endpoints:
assert endpoint_key in API_ENDPOINT_PERMISSIONS
assert API_ENDPOINT_PERMISSIONS[endpoint_key]["module"] == "fees"
```
## 集成测试
### 1. 权限管理API测试
**文件**: `backend/tests/test_api_permissions_endpoints.py`
```python
import pytest
from fastapi.testclient import TestClient
@pytest.mark.asyncio
async def test_list_api_endpoints(client: TestClient, admin_token: str):
"""测试获取所有API端点"""
response = client.get(
"/api/v1/api-permissions/endpoints",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == 200
data = response.json()
assert "endpoints" in data
assert len(data["endpoints"]) > 0
# 验证端点结构
for endpoint in data["endpoints"]:
assert "endpoint_key" in endpoint
assert "method" in endpoint
assert "path" in endpoint
assert "module" in endpoint
assert "action" in endpoint
@pytest.mark.asyncio
async def test_get_study_api_permissions(client: TestClient, pm_token: str, study_id: str):
"""测试获取项目的权限矩阵"""
response = client.get(
f"/api/v1/studies/{study_id}/api-permissions",
headers={"Authorization": f"Bearer {pm_token}"}
)
assert response.status_code == 200
data = response.json()
# 验证权限矩阵结构
for role, endpoints in data.items():
assert isinstance(endpoints, dict)
for endpoint_key, permission in endpoints.items():
assert "allowed" in permission
assert isinstance(permission["allowed"], bool)
@pytest.mark.asyncio
async def test_update_study_api_permissions(client: TestClient, pm_token: str, study_id: str):
"""测试更新项目的权限矩阵"""
payload = {
"CRA": {
"POST:/subjects": True,
"GET:/subjects": True,
"PATCH:/subjects/{id}": True,
},
"PV": {
"GET:/subjects": True,
"GET:/subjects/{id}": True,
}
}
response = client.put(
f"/api/v1/studies/{study_id}/api-permissions",
json=payload,
headers={"Authorization": f"Bearer {pm_token}"}
)
assert response.status_code == 200
# 验证权限已更新
response = client.get(
f"/api/v1/studies/{study_id}/api-permissions",
headers={"Authorization": f"Bearer {pm_token}"}
)
data = response.json()
assert data["CRA"]["POST:/subjects"]["allowed"] is True
assert data["PV"]["POST:/subjects"]["allowed"] is False
```
### 2. 迁移模块功能测试
**文件**: `backend/tests/test_migrated_endpoints.py`
```python
import pytest
from fastapi.testclient import TestClient
@pytest.mark.asyncio
async def test_create_subject_with_api_permission(client: TestClient, cra_token: str, study_id: str):
"""测试创建参与者 - 使用接口级权限"""
payload = {
"subject_no": "SUBJ001",
"site_id": "site-uuid",
"status": "ACTIVE",
}
response = client.post(
f"/api/v1/studies/{study_id}/subjects",
json=payload,
headers={"Authorization": f"Bearer {cra_token}"}
)
assert response.status_code == 201
@pytest.mark.asyncio
async def test_create_subject_without_permission(client: TestClient, pv_token: str, study_id: str):
"""测试创建参与者 - 权限不足"""
payload = {
"subject_no": "SUBJ001",
"site_id": "site-uuid",
"status": "ACTIVE",
}
response = client.post(
f"/api/v1/studies/{study_id}/subjects",
json=payload,
headers={"Authorization": f"Bearer {pv_token}"}
)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_list_subjects_with_permission(client: TestClient, cra_token: str, study_id: str):
"""测试查询参与者列表 - 有权限"""
response = client.get(
f"/api/v1/studies/{study_id}/subjects",
headers={"Authorization": f"Bearer {cra_token}"}
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_create_ae_with_permission(client: TestClient, cra_token: str, study_id: str):
"""测试创建不良事件 - 使用接口级权限"""
payload = {
"term": "Headache",
"onset_date": "2026-05-13",
"seriousness": "MILD",
}
response = client.post(
f"/api/v1/studies/{study_id}/aes",
json=payload,
headers={"Authorization": f"Bearer {cra_token}"}
)
assert response.status_code == 201
@pytest.mark.asyncio
async def test_create_contract_fee_with_permission(client: TestClient, pm_token: str):
"""测试创建费用合同 - 使用接口级权限"""
payload = {
"projectId": "project-uuid",
"centerId": "center-uuid",
"contractAmount": 10000,
"contractCases": 100,
}
response = client.post(
"/api/v1/fees/contracts",
json=payload,
headers={"Authorization": f"Bearer {pm_token}"}
)
assert response.status_code == 201
```
## 端到端测试
### 1. 权限变更流程
**场景**: 管理员修改权限后,用户权限立即生效
```python
@pytest.mark.asyncio
async def test_permission_change_takes_effect_immediately(
client: TestClient, pm_token: str, cra_token: str, study_id: str
):
"""测试权限变更立即生效"""
# 1. 初始状态:CRA可以创建参与者
payload = {"subject_no": "SUBJ001", "site_id": "site-uuid"}
response = client.post(
f"/api/v1/studies/{study_id}/subjects",
json=payload,
headers={"Authorization": f"Bearer {cra_token}"}
)
assert response.status_code == 201
# 2. PM修改权限:禁止CRA创建参与者
perm_payload = {
"CRA": {
"POST:/subjects": False,
}
}
response = client.put(
f"/api/v1/studies/{study_id}/api-permissions",
json=perm_payload,
headers={"Authorization": f"Bearer {pm_token}"}
)
assert response.status_code == 200
# 3. CRA尝试创建参与者:应该被拒绝
payload = {"subject_no": "SUBJ002", "site_id": "site-uuid"}
response = client.post(
f"/api/v1/studies/{study_id}/subjects",
json=payload,
headers={"Authorization": f"Bearer {cra_token}"}
)
assert response.status_code == 403
```
### 2. 跨模块数据访问
**场景**: PV创建不良事件时需要读取参与者信息
```python
@pytest.mark.asyncio
async def test_cross_module_data_access(
client: TestClient, pv_token: str, study_id: str, subject_id: str
):
"""测试跨模块数据访问权限"""
# 1. PV查询参与者信息(需要GET:/subjects/{id}权限)
response = client.get(
f"/api/v1/studies/{study_id}/subjects/{subject_id}",
headers={"Authorization": f"Bearer {pv_token}"}
)
assert response.status_code == 200
# 2. PV创建不良事件(需要POST:/risk-issues权限)
payload = {
"term": "Headache",
"onset_date": "2026-05-13",
"seriousness": "MILD",
"subject_id": subject_id,
}
response = client.post(
f"/api/v1/studies/{study_id}/aes",
json=payload,
headers={"Authorization": f"Bearer {pv_token}"}
)
assert response.status_code == 201
```
## 性能测试
### 1. 权限检查性能
```python
@pytest.mark.asyncio
async def test_permission_check_performance(db_session, study_id):
"""测试权限检查性能"""
import time
start = time.time()
for _ in range(1000):
await role_has_api_permission(
db_session, study_id, "CRA", "POST:/subjects"
)
elapsed = time.time() - start
# 1000次权限检查应该在1秒内完成
assert elapsed < 1.0
```
## 向后兼容测试
### 1. 模块级权限回退
```python
@pytest.mark.asyncio
async def test_fallback_to_module_level_permission(db_session, study_id):
"""测试回退到模块级权限"""
# 不创建接口级权限,应该使用模块级权限
result = await role_has_api_permission(
db_session, study_id, "CRA", "POST:/subjects"
)
# 结果应该基于模块级权限配置
assert isinstance(result, bool)
```
## 运行测试
```bash
# 运行所有测试
pytest backend/tests/
# 运行特定测试文件
pytest backend/tests/test_api_permissions.py
# 运行特定测试
pytest backend/tests/test_api_permissions.py::test_api_permission_check_allowed
# 运行并显示覆盖率
pytest backend/tests/ --cov=app --cov-report=html
```
## 验证清单
- [ ] 所有单元测试通过
- [ ] 所有集成测试通过
- [ ] 所有端到端测试通过
- [ ] 性能测试通过(权限检查 < 1ms)
- [ ] 向后兼容测试通过
- [ ] 代码覆盖率 > 80%
- [ ] 没有安全漏洞
- [ ] 文档完整