feat: Go后端LDAP真实对接 — AES-256-GCM密码加解密 + 登录LDAP fallback

- 新增 crypto.go: AES-256-GCM 加解密,与 Python backend 兼容
- auth.go: Login 添加 LDAP fallback(本地优先→LDAP绑定→自动创建本地用户)
- ldap.go: 实现真实 LDAP 连接/用户搜索/密码验证/OU列表/用户列表
- 所有 LDAP 操作解密 admin_password 后使用
- 新增/更新密码存储时加密
- router.go: 补充 delete-user 路由
- 添加 go-ldap/v3 依赖
This commit is contained in:
logan committed 2026-05-25 23:32:15 +08:00
1 parent 622923821b
commit 88e5f4e7ee
71 files changed
+20390 -64

No files matched your search

+280
View File
@@ -0,0 +1,280 @@
"""风险等级评估 + 维护窗口 + 通知脱敏 + SQL items 测试。"""
import json
import pytest
from unittest.mock import patch, MagicMock
# ===== 风险等级评估测试 =====
class TestRiskEvaluator:
"""风险等级自动计算测试。"""
def test_select_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("SELECT * FROM users WHERE id = 1") == "low"
def test_show_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("SHOW TABLES") == "low"
def test_explain_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("EXPLAIN SELECT * FROM users") == "low"
def test_create_table_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("CREATE TABLE t1 (id INT)") == "medium"
def test_alter_table_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("ALTER TABLE users ADD COLUMN age INT") == "medium"
def test_insert_with_where_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"INSERT INTO users (name) VALUES ('test')",
sql_type="DML",
) == "medium"
def test_update_without_where_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("UPDATE users SET status = 1") == "high"
def test_delete_without_where_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DELETE FROM users") == "high"
def test_alter_drop_column_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("ALTER TABLE users DROP COLUMN age") == "high"
def test_alter_modify_column_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("ALTER TABLE users MODIFY COLUMN age BIGINT") == "high"
def test_drop_table_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DROP TABLE users") == "critical"
def test_truncate_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("TRUNCATE TABLE users") == "critical"
def test_drop_database_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DROP DATABASE mydb") == "critical"
def test_many_statements_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"SELECT 1", statement_count=10
) == "high"
def test_multiple_audit_errors_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
findings = [
{"level": "error", "rule_id": "R1"},
{"level": "error", "rule_id": "R2"},
]
assert evaluate_risk_level(
"UPDATE users SET x=1 WHERE id=1",
sql_type="DML",
audit_findings=findings,
) == "high"
def test_delete_with_where_no_limit_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"DELETE FROM users WHERE id > 100"
) == "medium"
def test_delete_with_where_and_limit_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"DELETE FROM users WHERE id > 100 LIMIT 10"
) == "low"
# Redis 风险
def test_redis_flushall_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("FLUSHALL", db_type="redis") == "critical"
def test_redis_flushdb_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("FLUSHDB", db_type="redis") == "critical"
def test_redis_set_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("SET key value", db_type="redis") == "medium"
def test_redis_get_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("GET key", db_type="redis") == "low"
# MongoDB 风险
def test_mongo_drop_database_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("db.dropDatabase()", db_type="mongodb") == "critical"
def test_mongo_drop_database_upper_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DROP DATABASE mydb", db_type="mongodb") == "critical"
def test_mongo_insert_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("db.users.insertOne({name: 'test'})", db_type="mongodb") == "medium"
def test_mongo_find_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("db.users.find({})", db_type="mongodb") == "low"
# ===== 风险等级配置测试 =====
class TestRiskLevelConfig:
"""风险等级配置验证。"""
def test_all_levels_have_config(self):
from app.services.risk_evaluator import RISK_LEVEL_CONFIG
for level in ["low", "medium", "high", "critical"]:
assert level in RISK_LEVEL_CONFIG
cfg = RISK_LEVEL_CONFIG[level]
assert "label" in cfg
assert "color" in cfg
assert "min_approvers" in cfg
def test_high_and_critical_require_2_approvers(self):
from app.services.risk_evaluator import RISK_LEVEL_CONFIG
assert RISK_LEVEL_CONFIG["high"]["min_approvers"] >= 2
assert RISK_LEVEL_CONFIG["critical"]["min_approvers"] >= 2
# ===== 通知脱敏测试 =====
class TestNotificationMasking:
"""通知内容脱敏测试。"""
def test_mask_password_in_sql_preview(self):
from app.services.notification_sender import NotificationSender
data = {"sql_preview": "SET password='secret123' WHERE id=1"}
masked = NotificationSender._mask_sensitive_data(data)
assert "secret123" not in masked["sql_preview"]
assert "***" in masked["sql_preview"]
def test_mask_drop_table_in_sql_preview(self):
from app.services.notification_sender import NotificationSender
data = {"sql_preview": "DROP TABLE users"}
masked = NotificationSender._mask_sensitive_data(data)
assert "***" in masked["sql_preview"]
def test_mask_error_field(self):
from app.services.notification_sender import NotificationSender
data = {"error": "Connection failed: password='mysecret'"}
masked = NotificationSender._mask_sensitive_data(data)
assert "mysecret" not in masked["error"]
def test_truncate_long_sql_preview(self):
from app.services.notification_sender import NotificationSender
long_sql = "SELECT * FROM users WHERE " + "x=1 AND " * 50
data = {"sql_preview": long_sql}
masked = NotificationSender._mask_sensitive_data(data)
assert len(masked["sql_preview"]) <= 110 # 100 + "..."
def test_non_sensitive_data_unchanged(self):
from app.services.notification_sender import NotificationSender
data = {"title": "测试工单", "applicant": "张三"}
masked = NotificationSender._mask_sensitive_data(data)
assert masked["title"] == "测试工单"
assert masked["applicant"] == "张三"
# ===== 维护窗口测试 =====
class TestMaintenanceWindow:
"""维护窗口逻辑测试。"""
def test_no_config_allows_execution(self):
"""无配置时允许执行。"""
from app.api.v1.endpoints.ticket import _check_maintenance_window
from unittest.mock import AsyncMock
import asyncio
mock_db = AsyncMock()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_db.execute.return_value = mock_result
async def run():
result = await _check_maintenance_window(mock_db)
assert result is None
asyncio.get_event_loop().run_until_complete(run())
def test_disabled_window_allows_execution(self):
"""disabled 的窗口允许执行。"""
from app.api.v1.endpoints.ticket import _check_maintenance_window
from app.models.system_setting import SystemSetting
from unittest.mock import AsyncMock
import asyncio
setting = SystemSetting(key="maintenance_window", value='{"enabled": false}')
mock_db = AsyncMock()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = setting
mock_db.execute.return_value = mock_result
async def run():
result = await _check_maintenance_window(mock_db)
assert result is None
asyncio.get_event_loop().run_until_complete(run())
# ===== SQL 类型分类测试 =====
class TestClassifySqlType:
"""SQL 类型分类辅助函数测试。"""
def test_select_is_dql(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("SELECT * FROM t") == "DQL"
def test_insert_is_dml(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("INSERT INTO t VALUES (1)") == "DML"
def test_update_is_dml(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("UPDATE t SET x=1") == "DML"
def test_delete_is_dml(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("DELETE FROM t") == "DML"
def test_create_is_ddl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("CREATE TABLE t (id INT)") == "DDL"
def test_alter_is_ddl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("ALTER TABLE t ADD COLUMN x INT") == "DDL"
def test_drop_is_ddl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("DROP TABLE t") == "DDL"
def test_grant_is_dcl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("GRANT SELECT ON t TO user") == "DCL"
def test_redis_set(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("SET key value") == "SET"
def test_redis_get(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("GET key") == "SET"
def test_unknown(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("UNKNOWN COMMAND") == "OTHER"