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

+228
View File
@@ -0,0 +1,228 @@
"""系统设置 API 测试 — GET/PUT 设置 + 公共查询设置 + 查询集成。"""
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from tests.conftest import auth_header
async def _ensure_user(client: AsyncClient, admin_token: str, username: str, role: str = "developer") -> str:
"""确保用户存在并返回 token。"""
from app.core.security import hash_password, create_access_token
from app.models.user import User
from sqlalchemy import select
from app.core.database import get_db
# 通过 API 创建用户
resp = await client.post("/api/v1/users", headers=auth_header(admin_token), json={
"username": username, "password": "123456",
"display_name": username, "role": role
})
if resp.status_code in (200, 201):
data = resp.json()
user_id = data.get("id")
return create_access_token(data={"sub": str(user_id), "role": role})
# 如果已存在,尝试登录
resp = await client.post("/api/v1/auth/login", json={"username": username, "password": "123456"})
if resp.status_code == 200:
return resp.json()["access_token"]
# Fallback: 直接生成 token
raise RuntimeError(f"无法创建/获取用户 {username}: {resp.status_code} {resp.text}")
# ── 1. GET /settings ──
@pytest.mark.asyncio
async def test_get_settings(client, admin_token):
"""管理员可以获取所有系统设置。"""
token = admin_token
resp = await client.get("/api/v1/settings", headers=auth_header(token))
assert resp.status_code == 200
data = resp.json()
assert "query" in data
assert "security" in data
# 查询控制至少 3 项 (max_limit, timeout, auto_limit)
assert len(data["query"]) >= 3
@pytest.mark.asyncio
async def test_get_settings_non_admin(client, admin_token):
"""非管理员不能获取设置。"""
user_token = await _ensure_user(client, admin_token, "testuser_settings")
resp = await client.get("/api/v1/settings", headers=auth_header(user_token))
# require_role("admin") 应该返回 403
assert resp.status_code == 403
# ── 2. PUT /settings ──
@pytest.mark.asyncio
async def test_update_settings_boolean(client, admin_token):
"""更新布尔设置项。"""
token = admin_token
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_auto_limit", "value": "false"}
])
assert resp.status_code == 200
assert "query_auto_limit" in resp.json()["updated"]
# 验证
resp = await client.get("/api/v1/settings", headers=auth_header(token))
items = resp.json()["query"]
auto_limit = next(i for i in items if i["key"] == "query_auto_limit")
assert auto_limit["value"] == "false"
# 恢复
await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_auto_limit", "value": "true"}
])
@pytest.mark.asyncio
async def test_update_settings_integer(client, admin_token):
"""更新整数设置项。"""
token = admin_token
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_max_limit", "value": "5000"}
])
assert resp.status_code == 200
# 恢复
await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_max_limit", "value": "10000"}
])
@pytest.mark.asyncio
async def test_update_settings_validation(client, admin_token):
"""设置值校验。"""
token = admin_token
# 布尔值错误
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_auto_limit", "value": "yes"}
])
assert resp.status_code == 400
# 整数范围错误 (query_max_limit min=100)
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_max_limit", "value": "10"}
])
assert resp.status_code == 400
# 超时范围错误 (query_timeout min=5)
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_timeout", "value": "1"}
])
assert resp.status_code == 400
# ── 3. GET /settings/public/query ──
@pytest.mark.asyncio
async def test_public_query_settings(client):
"""公共查询设置无需鉴权。"""
resp = await client.get("/api/v1/settings/public/query")
assert resp.status_code == 200
data = resp.json()
assert "query_max_limit" in data
assert "query_timeout" in data
assert isinstance(data["query_max_limit"], int)
# ── 4. 查询集成 — 审批开关 ──
@pytest.mark.asyncio
async def test_query_require_approval(client, admin_token):
"""开启查询审批后普通用户无法直接查询。"""
user_token = await _ensure_user(client, admin_token, "testuser_approval")
# 开启查询审批
await client.put("/api/v1/settings", headers=auth_header(admin_token), json=[
{"key": "query_require_approval", "value": "true"}
])
# 普通用户查询应被拦截
resp = await client.post("/api/v1/query/execute", headers=auth_header(user_token), json={
"instance_id": 1, "database": "test", "sql": "SELECT 1"
})
assert resp.status_code == 403
assert "审批" in resp.json()["detail"]
# 管理员仍然可以查询(不被审批限制拦截,可能因连接报其他错)
resp = await client.post("/api/v1/query/execute", headers=auth_header(admin_token), json={
"instance_id": 1, "database": "test", "sql": "SELECT 1"
})
assert resp.status_code != 403, "管理员不应被审批限制拦截"
# 恢复
await client.put("/api/v1/settings", headers=auth_header(admin_token), json=[
{"key": "query_require_approval", "value": "false"}
])
# ── 5. 查询集成 — 语句长度检查 ──
@pytest.mark.asyncio
async def test_query_max_length(client, admin_token):
"""超长查询语句被拦截。"""
# 设置较短限制(在合法范围 500~50000 内)
await client.put("/api/v1/settings", headers=auth_header(admin_token), json=[
{"key": "query_max_length", "value": "500"}
])
# 超长 SQL(> 500 字符)
long_sql = "SELECT * FROM users WHERE id = " + "1" * 600
resp = await client.post("/api/v1/query/execute", headers=auth_header(admin_token), json={
"instance_id": 1, "database": "test", "sql": long_sql
})
assert resp.status_code == 400, f"Expected 400, got {resp.status_code}: {resp.text}"
assert "过长" in resp.json()["detail"]
# 正常长度 SQL 不被拦截(可能因连接失败但不应是400长度错误)
short_sql = "SELECT 1"
resp = await client.post("/api/v1/query/execute", headers=auth_header(admin_token), json={
"instance_id": 1, "database": "test", "sql": short_sql
})
assert resp.status_code != 400 or "过长" not in resp.text
# 恢复
await client.put("/api/v1/settings", headers=auth_header(admin_token), json=[
{"key": "query_max_length", "value": "50000"}
])
# ── 6. 批量更新 ──
@pytest.mark.asyncio
async def test_bulk_update(client, admin_token):
"""批量更新多个设置。"""
token = admin_token
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_timeout", "value": "60"},
{"key": "query_max_limit", "value": "2000"},
])
assert resp.status_code == 200
assert len(resp.json()["updated"]) == 2
# 恢复
await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_timeout", "value": "30"},
{"key": "query_max_limit", "value": "10000"},
])
# ── 7. 设置种子数据自动初始化 ──
@pytest.mark.asyncio
async def test_settings_auto_seed(client, admin_token):
"""设置接口自动初始化种子数据。"""
token = admin_token
resp = await client.get("/api/v1/settings", headers=auth_header(token))
assert resp.status_code == 200
data = resp.json()
# 确认至少 9 个设置项 (query:3 + security:2 + cleanup:3 + executor:1)
total = sum(len(v) for v in data.values())
assert total >= 9