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:
1 parent
622923821b
commit
88e5f4e7ee
71 files changed
+20390
-64
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user