权限监控与管理界面全面美化
重新设计权限系统所有 UI 组件的视觉风格,统一配色、圆角、阴影和交互动效: - 实时概览:统计卡片加图标和渐变色条,健康评分改为环形进度,告警改为卡片式 - 趋势分析:图表卡片加彩色图标标识,ECharts 配色升级为渐变面积填充 - 访问日志:指标卡片带图标,日志审计改为卡片式入口,IP排行前三高亮 - IP属地:工具栏重设计,排行列表前三渐变高亮,指标卡片统一新风格 - 系统级权限:从 el-table 改为自定义卡片列表,模块块独立圆角卡片 - 项目权限配置:空状态引导优化,成员表格加头像,工具栏加背景容器 - 角色概览卡片:加进度条可视化,hover 微动效 - 接口权限矩阵:工具栏分离布局,表格圆角包裹 - 角色管理抽屉:侧边栏选中态渐变,操作行 hover 高亮 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -409,7 +409,7 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
# 项目概览
|
||||
"overview:read": {
|
||||
"project_overview:read": {
|
||||
"module": "project_overview",
|
||||
"action": "read",
|
||||
"description": "查询项目概览",
|
||||
@@ -583,6 +583,13 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"default_roles": ["PM", "CRA"],
|
||||
"prerequisite_permissions": ["subjects:read", "sites:read"],
|
||||
},
|
||||
"subject_pds:delete": {
|
||||
"module": "subjects",
|
||||
"action": "write",
|
||||
"description": "删除参与者PDS",
|
||||
"default_roles": ["PM"],
|
||||
"prerequisite_permissions": ["subjects:read", "sites:read"],
|
||||
},
|
||||
# 审计日志管理
|
||||
"audit_logs:list": {
|
||||
"module": "audit_export",
|
||||
@@ -665,18 +672,43 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"],
|
||||
},
|
||||
# 项目里程碑管理
|
||||
"milestones:list": {
|
||||
"project_milestones:read": {
|
||||
"module": "project_milestones",
|
||||
"action": "read",
|
||||
"description": "查询项目里程碑列表",
|
||||
"default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"],
|
||||
},
|
||||
"milestones:update": {
|
||||
"project_milestones:update": {
|
||||
"module": "project_milestones",
|
||||
"action": "write",
|
||||
"description": "更新项目里程碑",
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
# 启动与授权
|
||||
"startup_auth:create": {
|
||||
"module": "startup_auth",
|
||||
"action": "write",
|
||||
"description": "创建启动会或培训授权",
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
"startup_auth:read": {
|
||||
"module": "startup_auth",
|
||||
"action": "read",
|
||||
"description": "查询启动会或培训授权",
|
||||
"default_roles": ["PM", "CRA", "PV", "IMP"],
|
||||
},
|
||||
"startup_auth:update": {
|
||||
"module": "startup_auth",
|
||||
"action": "write",
|
||||
"description": "更新启动会或培训授权",
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
"startup_auth:delete": {
|
||||
"module": "startup_auth",
|
||||
"action": "write",
|
||||
"description": "删除培训授权",
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
# 附件管理
|
||||
"attachments:create": {
|
||||
"module": "attachments",
|
||||
@@ -981,12 +1013,16 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
|
||||
},
|
||||
"startup_auth": {
|
||||
"read": [
|
||||
"startup_auth:read",
|
||||
"budget:list",
|
||||
"budget:read",
|
||||
"timeline:list",
|
||||
"timeline:read",
|
||||
],
|
||||
"write": [
|
||||
"startup_auth:create",
|
||||
"startup_auth:update",
|
||||
"startup_auth:delete",
|
||||
"budget:create",
|
||||
"budget:update",
|
||||
"budget:delete",
|
||||
@@ -997,7 +1033,7 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
|
||||
},
|
||||
"project_overview": {
|
||||
"read": [
|
||||
"overview:read",
|
||||
"project_overview:read",
|
||||
],
|
||||
"write": [],
|
||||
},
|
||||
@@ -1083,10 +1119,10 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = {
|
||||
},
|
||||
"project_milestones": {
|
||||
"read": [
|
||||
"milestones:list",
|
||||
"project_milestones:read",
|
||||
],
|
||||
"write": [
|
||||
"milestones:update",
|
||||
"project_milestones:update",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ class Settings(BaseSettings):
|
||||
LOGIN_RSA_KEY_ID: str = "default"
|
||||
LOGIN_CHALLENGE_TTL_SECONDS: int = 120
|
||||
LOGIN_CHALLENGE_MAX_ACTIVE: int = 1000
|
||||
IP2REGION_XDB_PATH: Optional[str] = None
|
||||
IP2REGION_IPV6_XDB_PATH: Optional[str] = None
|
||||
|
||||
|
||||
@lru_cache
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Annotated, AsyncGenerator, Callable, Iterable
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
@@ -129,12 +130,17 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
|
||||
check_prerequisites: 是否检查前置权限
|
||||
"""
|
||||
async def dependency(
|
||||
request: Request,
|
||||
study_id: uuid.UUID,
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
from app.core.permission_monitor import get_permission_monitor
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if allow_system_admin and role_value == "ADMIN":
|
||||
_enqueue_permission_log(
|
||||
study_id, current_user.id, endpoint_key, "ADMIN", True, 0.0, request
|
||||
)
|
||||
return current_user
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
@@ -143,11 +149,29 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
|
||||
message="不是该项目成员",
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
allowed = await role_has_api_permission(
|
||||
db, study_id, membership.role_in_study, endpoint_key, check_prerequisites=check_prerequisites
|
||||
)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
error = None
|
||||
try:
|
||||
allowed = await role_has_api_permission(
|
||||
db, study_id, membership.role_in_study, endpoint_key, check_prerequisites=check_prerequisites
|
||||
)
|
||||
except Exception as e:
|
||||
error = e
|
||||
allowed = False
|
||||
raise
|
||||
finally:
|
||||
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||
monitor = get_permission_monitor()
|
||||
monitor.record_permission_check(
|
||||
allowed=allowed, elapsed_time=elapsed_ms / 1000, error=error
|
||||
)
|
||||
_enqueue_permission_log(
|
||||
study_id, current_user.id, endpoint_key,
|
||||
membership.role_in_study, allowed, elapsed_ms, request
|
||||
)
|
||||
|
||||
if not allowed:
|
||||
# 获取缺失的前置权限,用于错误提示
|
||||
missing_prereqs = await get_missing_prerequisites(
|
||||
db, study_id, membership.role_in_study, endpoint_key
|
||||
)
|
||||
@@ -168,6 +192,32 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
|
||||
return dependency
|
||||
|
||||
|
||||
def _enqueue_permission_log(
|
||||
study_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
endpoint_key: str,
|
||||
role: str,
|
||||
allowed: bool,
|
||||
elapsed_ms: float,
|
||||
request: Request,
|
||||
) -> None:
|
||||
from app.services.permission_log_writer import get_log_writer
|
||||
|
||||
writer = get_log_writer()
|
||||
if writer:
|
||||
forwarded = request.headers.get("x-forwarded-for")
|
||||
ip = forwarded.split(",")[0].strip() if forwarded else (request.client.host if request.client else None)
|
||||
writer.enqueue({
|
||||
"study_id": study_id,
|
||||
"user_id": user_id,
|
||||
"endpoint_key": endpoint_key,
|
||||
"role": role,
|
||||
"allowed": allowed,
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"ip_address": ip,
|
||||
})
|
||||
|
||||
|
||||
async def get_cra_site_scope(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy import delete, select
|
||||
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.permission_cache import get_permission_cache
|
||||
|
||||
@@ -102,7 +103,10 @@ async def get_api_endpoint_permissions(
|
||||
"""
|
||||
overrides = await _get_project_permission_overrides(db, study_id)
|
||||
|
||||
roles = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"]
|
||||
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()]))
|
||||
matrix: dict[str, dict[str, dict[str, bool]]] = {}
|
||||
for role in roles:
|
||||
matrix[role] = {}
|
||||
|
||||
Reference in New Issue
Block a user