权限监控与管理界面全面美化
重新设计权限系统所有 UI 组件的视觉风格,统一配色、圆角、阴影和交互动效: - 实时概览:统计卡片加图标和渐变色条,健康评分改为环形进度,告警改为卡片式 - 趋势分析:图表卡片加彩色图标标识,ECharts 配色升级为渐变面积填充 - 访问日志:指标卡片带图标,日志审计改为卡片式入口,IP排行前三高亮 - IP属地:工具栏重设计,排行列表前三渐变高亮,指标卡片统一新风格 - 系统级权限:从 el-table 改为自定义卡片列表,模块块独立圆角卡片 - 项目权限配置:空状态引导优化,成员表格加头像,工具栏加背景容器 - 角色概览卡片:加进度条可视化,hover 微动效 - 接口权限矩阵:工具栏分离布局,表格圆角包裹 - 角色管理抽屉:侧边栏选中态渐变,操作行 hover 高亮 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,19 @@
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.permission_monitor import get_permission_monitor, set_permission_monitor, PermissionMonitor
|
||||
from app.api.v1 import permission_monitoring
|
||||
|
||||
|
||||
class FakeIpInfo:
|
||||
def __init__(self, province: str, city: str) -> None:
|
||||
self.country = "中国"
|
||||
self.province = province
|
||||
self.city = city
|
||||
self.isp = "电信"
|
||||
self.location = f"中国 / {province} / {city} / 电信"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -205,3 +216,242 @@ async def test_permission_system_health_includes_metrics(client: TestClient, aut
|
||||
assert "cache_stats" in data
|
||||
assert "check_metrics" in data["metrics"]
|
||||
assert "cache_metrics" in data["metrics"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ip_locations_counts_unique_users_per_location(db_session, monkeypatch):
|
||||
"""IP 属地统计应按省市聚合访问次数、来源 IP 数和访问用户数。"""
|
||||
await db_session.execute(text("DELETE FROM permission_access_logs"))
|
||||
await db_session.commit()
|
||||
|
||||
study_id = "00000000-0000-0000-0000-000000000001"
|
||||
user_a = "00000000-0000-0000-0000-000000000101"
|
||||
user_b = "00000000-0000-0000-0000-000000000102"
|
||||
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": study_id,
|
||||
"code": "IP-LOCATION-STUDY",
|
||||
"name": "IP Location Study",
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": "[]",
|
||||
},
|
||||
)
|
||||
for user_id, email in [(user_a, "user-a@example.com"), (user_b, "user-b@example.com")]:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO users (id, email, password_hash, full_name, role, clinical_department, status)
|
||||
VALUES (:id, :email, :password_hash, :full_name, :role, :clinical_department, :status)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"password_hash": "hash",
|
||||
"full_name": email,
|
||||
"role": "PM",
|
||||
"clinical_department": "临床运营",
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
)
|
||||
|
||||
rows = [
|
||||
(study_id, user_a, "10.1.1.1", True),
|
||||
(study_id, user_a, "10.1.1.1", False),
|
||||
(study_id, user_b, "10.1.1.2", True),
|
||||
]
|
||||
for study, user, ip_address, allowed in rows:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO permission_access_logs
|
||||
(id, study_id, user_id, endpoint_key, role, allowed, elapsed_ms, ip_address, created_at)
|
||||
VALUES
|
||||
(lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
|
||||
substr(lower(hex(randomblob(2))),2) || '-' ||
|
||||
substr('89ab', abs(random()) % 4 + 1, 1) ||
|
||||
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
|
||||
:study_id, :user_id, :endpoint_key, :role, :allowed, :elapsed_ms, :ip_address, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"study_id": study,
|
||||
"user_id": user,
|
||||
"endpoint_key": "admin.permissions.read",
|
||||
"role": "PM",
|
||||
"allowed": allowed,
|
||||
"elapsed_ms": 3.2,
|
||||
"ip_address": ip_address,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
monkeypatch.setattr(
|
||||
permission_monitoring,
|
||||
"resolve_ip_location",
|
||||
lambda ip: FakeIpInfo("广东省", "深圳市"),
|
||||
)
|
||||
|
||||
result = await permission_monitoring.get_ip_locations(db=db_session, _=object(), days=7, limit=10)
|
||||
|
||||
assert result["items"][0]["province"] == "广东省"
|
||||
assert result["items"][0]["total_count"] == 3
|
||||
assert result["items"][0]["unique_ip_count"] == 2
|
||||
assert result["items"][0]["unique_user_count"] == 2
|
||||
assert result["summary"]["total_count"] == 3
|
||||
assert result["summary"]["unique_ip_count"] == 2
|
||||
assert result["summary"]["unique_user_count"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_logs_include_user_behavior_summary(db_session):
|
||||
"""访问日志应返回用户行为审计汇总和用户排行。"""
|
||||
await db_session.execute(text("DELETE FROM permission_access_logs"))
|
||||
await db_session.commit()
|
||||
|
||||
study_id = "00000000-0000-0000-0000-000000000201"
|
||||
user_a = "00000000-0000-0000-0000-000000000301"
|
||||
user_b = "00000000-0000-0000-0000-000000000302"
|
||||
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": study_id,
|
||||
"code": "ACCESS-LOG-STUDY",
|
||||
"name": "Access Log Study",
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": "[]",
|
||||
},
|
||||
)
|
||||
for user_id, email, name, role in [
|
||||
(user_a, "audit-a@example.com", "审计用户A", "PM"),
|
||||
(user_b, "audit-b@example.com", "审计用户B", "CRA"),
|
||||
]:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO users (id, email, password_hash, full_name, role, clinical_department, status)
|
||||
VALUES (:id, :email, :password_hash, :full_name, :role, :clinical_department, :status)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"password_hash": "hash",
|
||||
"full_name": name,
|
||||
"role": role,
|
||||
"clinical_department": "临床运营",
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
)
|
||||
|
||||
rows = [
|
||||
(study_id, user_a, "admin.permissions.read", "PM", True, 3.0, "10.1.1.1"),
|
||||
(study_id, user_a, "admin.users.delete", "PM", False, 9.0, "10.1.1.2"),
|
||||
(study_id, user_b, "admin.permissions.read", "CRA", True, 6.0, "10.1.1.3"),
|
||||
]
|
||||
for study, user, endpoint, role, allowed, elapsed_ms, ip_address in rows:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO permission_access_logs
|
||||
(id, study_id, user_id, endpoint_key, role, allowed, elapsed_ms, ip_address, created_at)
|
||||
VALUES
|
||||
(lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
|
||||
substr(lower(hex(randomblob(2))),2) || '-' ||
|
||||
substr('89ab', abs(random()) % 4 + 1, 1) ||
|
||||
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
|
||||
:study_id, :user_id, :endpoint_key, :role, :allowed, :elapsed_ms, :ip_address, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"study_id": study,
|
||||
"user_id": user,
|
||||
"endpoint_key": endpoint,
|
||||
"role": role,
|
||||
"allowed": allowed,
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"ip_address": ip_address,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
result = await permission_monitoring.get_access_logs(
|
||||
db=db_session,
|
||||
_=object(),
|
||||
study_id=None,
|
||||
user_id=None,
|
||||
endpoint_key=None,
|
||||
role=None,
|
||||
allowed=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
assert result["summary"]["total_count"] == 3
|
||||
assert result["summary"]["unique_user_count"] == 2
|
||||
assert result["summary"]["denied_count"] == 1
|
||||
assert result["summary"]["avg_elapsed_ms"] == 6.0
|
||||
assert result["user_stats"][0]["user_name"] == "审计用户A"
|
||||
assert result["user_stats"][0]["total_count"] == 1
|
||||
assert result["user_stats"][0]["denied_count"] in {0, 1}
|
||||
assert result["user_stats"][0]["unique_ip_count"] == 1
|
||||
assert result["user_stats"][0]["sample_ip_address"] in {"10.1.1.1", "10.1.1.2"}
|
||||
user_a_ips = {
|
||||
stat["sample_ip_address"]
|
||||
for stat in result["user_stats"]
|
||||
if stat["user_name"] == "审计用户A"
|
||||
}
|
||||
assert user_a_ips == {"10.1.1.1", "10.1.1.2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_security_access_logs_include_anonymous_ip_attempts(db_session):
|
||||
"""安全访问日志应覆盖未登录或未知账号的底层访问尝试。"""
|
||||
await db_session.execute(text("DELETE FROM security_access_logs"))
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO security_access_logs
|
||||
(id, method, path, status_code, elapsed_ms, client_ip, user_agent, auth_status, user_identifier, created_at)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000501', 'POST', '/api/v1/auth/login', 401, 12.5,
|
||||
'203.0.113.10', 'attack-bot/1.0', 'ANONYMOUS', NULL, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
result = await permission_monitoring.get_security_access_logs(
|
||||
db=db_session,
|
||||
_=object(),
|
||||
status_min=400,
|
||||
auth_status=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
assert result["summary"]["total_count"] == 1
|
||||
assert result["summary"]["anonymous_count"] == 1
|
||||
assert result["summary"]["error_count"] == 1
|
||||
assert result["items"][0]["client_ip"] == "203.0.113.10"
|
||||
assert result["items"][0]["account_label"] == "未知账号"
|
||||
assert result["items"][0]["status_code"] == 401
|
||||
|
||||
Reference in New Issue
Block a user