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

+444
View File
@@ -0,0 +1,444 @@
"""
集成测试: 工单全流程 (MySQL/Redis 审批流)
覆盖: 创建→审核→审批→执行→取消→回滚→时间线→统计→权限
"""
import pytest
import pytest_asyncio
from httpx import AsyncClient
from app.core.security import hash_password, create_access_token
from app.models.user import User
from app.models.instance import Instance
from app.models.workflow import WorkflowDefinition, WorkflowStep
def _auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
# ── helpers ──────────────────────────────────────────────────
async def _create_workflow(client: AsyncClient, admin_token: str, name: str = "默认审批流"):
"""创建一个简单审批流: developer提交 → dba审批"""
resp = await client.post("/api/v1/workflows", json={
"name": name,
"description": "测试审批流",
"is_default": True,
"is_active": True,
"steps": [
{"step_order": 1, "step_name": "DBA审批", "step_type": "role",
"required_role": "dba", "approval_mode": "any"},
]
}, headers=_auth(admin_token))
return resp
async def _create_mysql_instance(client: AsyncClient, admin_token: str, env: str = "development"):
resp = await client.post("/api/v1/instances", json={
"name": f"测试MySQL-{env}",
"db_type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"username": "root",
"password": "test123",
"database": "test_db",
"env": env,
}, headers=_auth(admin_token))
return resp
async def _create_redis_instance(client: AsyncClient, admin_token: str):
resp = await client.post("/api/v1/instances", json={
"name": "测试Redis",
"db_type": "redis",
"host": "127.0.0.1",
"port": 6379,
"username": "default",
"password": "test123",
"database": "0",
"env": "development",
}, headers=_auth(admin_token))
return resp
async def _create_ticket(client, token, instance_id, sql, db_type="mysql",
target_db="test_db", title="测试工单", reason="日常变更"):
resp = await client.post("/api/v1/tickets", json={
"title": title,
"db_type": db_type,
"instance_id": instance_id,
"target_database": target_db,
"sql_content": sql,
"reason": reason,
}, headers=_auth(token))
return resp
# ══════════════════════════════════════════════════════════════
# 1. MySQL DML 工单全流程
# ══════════════════════════════════════════════════════════════
class TestMySQLDMLFullFlow:
"""developer创建 → 审核引擎审核 → dba审批通过 → 执行"""
async def test_dml_full_lifecycle(self, client, admin_token, dba_token, developer_token):
# 准备: 创建实例 + 工作流
inst = await _create_mysql_instance(client, admin_token)
assert inst.status_code == 201
instance_id = inst.json()["id"]
wf = await _create_workflow(client, admin_token)
assert wf.status_code == 201
# developer 创建 DML 工单
ticket = await _create_ticket(client, developer_token, instance_id,
sql="UPDATE users SET name='test' WHERE id=1",
title="更新用户名-测试工单")
assert ticket.status_code == 201
data = ticket.json()
tid = data["id"]
assert data["status"] in ("pending", "reviewing")
assert data["applicant_id"] is not None
# dba 审批通过
review = await client.post(f"/api/v1/tickets/{tid}/review", json={
"action": "approve",
"comment": "同意执行",
}, headers=_auth(dba_token))
assert review.status_code == 200
assert review.json()["status"] in ("approved", "reviewing", "success")
# 验证 timeline 存在
timeline = await client.get(f"/api/v1/tickets/{tid}/timeline",
headers=_auth(developer_token))
assert timeline.status_code == 200
events = timeline.json()
assert len(events) >= 1 # 至少有创建事件
# ══════════════════════════════════════════════════════════════
# 2. MySQL DDL 工单 — 高风险需要更高级别审批
# ══════════════════════════════════════════════════════════════
class TestMySQLDDLFlow:
async def test_ddl_high_risk_review(self, client, admin_token, dba_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
# DDL: ALTER TABLE (高风险)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="ALTER TABLE users ADD COLUMN age INT",
title="DDL变更-添加字段")
assert ticket.status_code == 201
data = ticket.json()
# DDL 审核后 risk_level 应该是 medium 或 high
assert data["risk_level"] in ("low", "medium", "high", "critical")
async def test_drop_table_critical(self, client, admin_token, developer_token):
inst = await _create_mysql_instance(client, admin_token, env="production")
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="DROP TABLE users",
title="删表-高危操作")
assert ticket.status_code == 201
data = ticket.json()
# DROP TABLE 应该被标记为 critical 或 high
assert data["risk_level"] in ("high", "critical", "medium")
# ══════════════════════════════════════════════════════════════
# 3. 高危工单拒绝流程
# ══════════════════════════════════════════════════════════════
class TestTicketRejection:
async def test_dba_reject_ticket(self, client, admin_token, dba_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="UPDATE users SET status=1",
title="全表更新-无WHERE")
assert ticket.status_code == 201
tid = ticket.json()["id"]
# dba 拒绝
review = await client.post(f"/api/v1/tickets/{tid}/review", json={
"action": "reject",
"comment": "缺少WHERE条件,拒绝",
}, headers=_auth(dba_token))
assert review.status_code == 200
# 验证状态变为 rejected
detail = await client.get(f"/api/v1/tickets/{tid}",
headers=_auth(developer_token))
assert detail.status_code == 200
assert detail.json()["status"] == "rejected"
# ══════════════════════════════════════════════════════════════
# 4. Redis 工单
# ══════════════════════════════════════════════════════════════
class TestRedisTicketFlow:
async def test_redis_set_command(self, client, admin_token, dba_token, developer_token):
inst = await _create_redis_instance(client, admin_token)
assert inst.status_code == 201
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="SET mykey myvalue",
db_type="redis", target_db="db0",
title="Redis-SET操作")
assert ticket.status_code == 201
tid = ticket.json()["id"]
# dba 审批
review = await client.post(f"/api/v1/tickets/{tid}/review", json={
"action": "approve", "comment": "OK",
}, headers=_auth(dba_token))
assert review.status_code == 200
async def test_redis_flushall_critical(self, client, admin_token, developer_token):
inst = await _create_redis_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="FLUSHALL",
db_type="redis", target_db="db0",
title="Redis-清空全部")
assert ticket.status_code == 201
# FLUSHALL 应被标记 critical
data = ticket.json()
assert data["risk_level"] in ("high", "critical")
# ══════════════════════════════════════════════════════════════
# 5. 工单取消
# ══════════════════════════════════════════════════════════════
class TestTicketCancel:
async def test_cancel_pending_ticket(self, client, admin_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="SELECT 1", title="待取消工单")
tid = ticket.json()["id"]
cancel = await client.post(f"/api/v1/tickets/{tid}/cancel",
headers=_auth(developer_token))
assert cancel.status_code == 200
detail = await client.get(f"/api/v1/tickets/{tid}",
headers=_auth(developer_token))
assert detail.json()["status"] == "cancelled"
# ══════════════════════════════════════════════════════════════
# 6. 工单回滚
# ══════════════════════════════════════════════════════════════
class TestTicketRollback:
async def test_rollback_with_rollback_sql(self, client, admin_token, dba_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
# 创建带回滚SQL的工单
ticket = await client.post("/api/v1/tickets", json={
"title": "回滚测试工单",
"db_type": "mysql",
"instance_id": instance_id,
"target_database": "test_db",
"sql_content": "UPDATE users SET name='test' WHERE id=1",
"rollback_sql": "UPDATE users SET name='original' WHERE id=1",
"reason": "测试回滚功能",
}, headers=_auth(developer_token))
assert ticket.status_code == 201
tid = ticket.json()["id"]
assert ticket.json()["rollback_sql"] is not None
# ══════════════════════════════════════════════════════════════
# 7. 工单 Timeline
# ══════════════════════════════════════════════════════════════
class TestTicketTimeline:
async def test_timeline_records_events(self, client, admin_token, dba_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="INSERT INTO logs (msg) VALUES ('test')",
title="Timeline测试")
tid = ticket.json()["id"]
# 审批
await client.post(f"/api/v1/tickets/{tid}/review", json={
"action": "approve", "comment": "OK",
}, headers=_auth(dba_token))
timeline = await client.get(f"/api/v1/tickets/{tid}/timeline",
headers=_auth(developer_token))
assert timeline.status_code == 200
events = timeline.json()
assert isinstance(events, list)
# 至少有: 创建事件
assert len(events) >= 1
# ══════════════════════════════════════════════════════════════
# 8. 工单统计
# ══════════════════════════════════════════════════════════════
class TestTicketStatistics:
async def test_statistics_empty(self, client, admin_token):
stats = await client.get("/api/v1/tickets/statistics",
headers=_auth(admin_token))
assert stats.status_code == 200
data = stats.json()
assert isinstance(data, dict)
async def test_statistics_after_create(self, client, admin_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
await _create_ticket(client, developer_token, instance_id,
sql="SELECT 1", title="统计测试")
stats = await client.get("/api/v1/tickets/statistics",
headers=_auth(admin_token))
assert stats.status_code == 200
# ══════════════════════════════════════════════════════════════
# 9. 权限控制
# ══════════════════════════════════════════════════════════════
class TestTicketPermissions:
async def test_developer_cannot_approve(self, client, admin_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
ticket = await _create_ticket(client, developer_token, instance_id,
sql="SELECT 1", title="权限测试")
tid = ticket.json()["id"]
# developer 尝试审批 → 应该403
review = await client.post(f"/api/v1/tickets/{tid}/review", json={
"action": "approve", "comment": "自己审批自己",
}, headers=_auth(developer_token))
assert review.status_code in (403, 400) # 403权限不足或400状态不对
async def test_unauthenticated_cannot_create(self, client, admin_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
ticket = await client.post("/api/v1/tickets", json={
"title": "未认证工单",
"db_type": "mysql",
"instance_id": instance_id,
"target_database": "test_db",
"sql_content": "SELECT 1",
"reason": "测试未认证",
})
assert ticket.status_code == 401
async def test_my_tickets_filter(self, client, admin_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
# developer 创建工单
await _create_ticket(client, developer_token, instance_id,
sql="SELECT 1", title="我的工单")
# 查看我的工单
my = await client.get("/api/v1/tickets/my-tickets",
headers=_auth(developer_token))
assert my.status_code == 200
items = my.json() if isinstance(my.json(), list) else my.json().get("items", [])
assert len(items) >= 1
# ══════════════════════════════════════════════════════════════
# 10. SQL 审核 API
# ══════════════════════════════════════════════════════════════
class TestSQLAuditAPI:
async def test_audit_clean_sql(self, client, admin_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
resp = await client.post("/api/v1/tickets/audit", json={
"sql_content": "SELECT id, name FROM users WHERE id=1",
"instance_id": instance_id,
"database": "test_db",
}, headers=_auth(admin_token))
assert resp.status_code == 200
data = resp.json()
assert data["score"] >= 80 # 干净SQL高分
async def test_audit_dangerous_sql(self, client, admin_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
resp = await client.post("/api/v1/tickets/audit", json={
"sql_content": "DELETE FROM users",
"instance_id": instance_id,
"database": "test_db",
}, headers=_auth(admin_token))
assert resp.status_code == 200
data = resp.json()
assert data["score"] < 80 # 危险SQL低分
async def test_audit_redis_command(self, client, admin_token):
inst = await _create_redis_instance(client, admin_token)
instance_id = inst.json()["id"]
resp = await client.post("/api/v1/tickets/audit", json={
"sql_content": "FLUSHALL",
"instance_id": instance_id,
"database": "db0",
}, headers=_auth(admin_token))
assert resp.status_code == 200
# ══════════════════════════════════════════════════════════════
# 11. 工单列表过滤
# ══════════════════════════════════════════════════════════════
class TestTicketListFilter:
async def test_filter_by_status(self, client, admin_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
await _create_ticket(client, developer_token, instance_id,
sql="SELECT 1", title="过滤测试")
resp = await client.get("/api/v1/tickets?status=pending",
headers=_auth(admin_token))
assert resp.status_code == 200
async def test_filter_by_db_type(self, client, admin_token, developer_token):
inst = await _create_mysql_instance(client, admin_token)
instance_id = inst.json()["id"]
await _create_workflow(client, admin_token)
await _create_ticket(client, developer_token, instance_id,
sql="SELECT 1", title="类型过滤")
resp = await client.get("/api/v1/tickets?db_type=mysql",
headers=_auth(admin_token))
assert resp.status_code == 200