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

+227
View File
@@ -0,0 +1,227 @@
"""数据字典模块测试。"""
import sys
from unittest.mock import patch, AsyncMock, MagicMock
from httpx import AsyncClient
from tests.conftest import auth_header
from app.services.db_connector import QueryResult
def _mock_query_result(columns, rows):
"""Helper: build a QueryResult for mocking execute_query."""
return QueryResult(
columns=columns,
rows=rows,
total=len(rows),
elapsed_ms=10,
truncated=False,
)
class TestListDatabases:
async def test_list_databases_success(
self, client: AsyncClient, test_instance, admin_token
):
mock_result = _mock_query_result(
["Database"],
[
{"Database": "test_db"},
{"Database": "app_db"},
{"Database": "information_schema"},
],
)
with patch(
"app.services.dictionary_service.DBConnector.execute_query",
new_callable=AsyncMock,
return_value=mock_result,
):
resp = await client.get(
"/api/v1/dictionary/instances/" + str(test_instance.id) + "/databases",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert "databases" in data
db_list = data["databases"]
assert "test_db" in db_list
assert "app_db" in db_list
# system databases should be filtered
assert "information_schema" not in db_list
async def test_list_databases_not_found(
self, client: AsyncClient, admin_token
):
resp = await client.get(
"/api/v1/dictionary/instances/99999/databases",
headers=auth_header(admin_token),
)
assert resp.status_code == 404
class TestListTables:
async def test_list_tables_success(
self, client: AsyncClient, test_instance, admin_token
):
mock_result = _mock_query_result(
["TABLE_NAME"],
[{"TABLE_NAME": "users"}, {"TABLE_NAME": "orders"}],
)
with patch(
"app.services.dictionary_service.DBConnector.execute_query",
new_callable=AsyncMock,
return_value=mock_result,
):
resp = await client.get(
"/api/v1/dictionary/instances/" + str(test_instance.id) + "/tables",
params={"database": "test_db"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert "tables" in data
assert "users" in data["tables"]
assert "orders" in data["tables"]
class TestGetTableDetail:
async def test_table_detail_success(
self, client: AsyncClient, test_instance, admin_token
):
col_result = _mock_query_result(
["Field", "Type", "Null", "Key", "Default", "Extra", "Comment"],
[
{
"Field": "id",
"Type": "int",
"Null": "NO",
"Key": "PRI",
"Default": None,
"Extra": "auto_increment",
"Comment": "primary key",
},
],
)
idx_result = _mock_query_result(
["Key_name", "Non_unique", "Index_type", "Column_name"],
[
{"Key_name": "PRIMARY", "Non_unique": 0, "Index_type": "BTREE", "Column_name": "id"},
],
)
ddl_result = _mock_query_result(
["Table", "Create Table"],
[{"Table": "users", "Create Table": "CREATE TABLE users (id int PRIMARY KEY)"}],
)
cnt_result = _mock_query_result(
["TABLE_ROWS"],
[{"TABLE_ROWS": 42}],
)
call_count = 0
results = [col_result, idx_result, ddl_result, cnt_result]
async def _mock_execute(*args, **kwargs):
nonlocal call_count
r = results[call_count]
call_count += 1
return r
with patch(
"app.services.dictionary_service.DBConnector.execute_query",
new=AsyncMock(side_effect=_mock_execute),
):
resp = await client.get(
"/api/v1/dictionary/instances/" + str(test_instance.id) + "/tables/users",
params={"database": "test_db"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert "columns" in data
assert len(data["columns"]) == 1
assert data["columns"][0]["Field"] == "id"
assert "indexes" in data
assert "create_ddl" in data
assert "row_count" in data
assert data["row_count"] == 42
class TestExportDictionary:
async def test_export_success(
self, client: AsyncClient, test_instance, admin_token
):
# list_tables returns 1 table
tbl_result = _mock_query_result(
["TABLE_NAME"],
[{"TABLE_NAME": "users"}],
)
# get_table_detail: columns
col_result = _mock_query_result(
["Field", "Type", "Null", "Key", "Default", "Extra", "Comment"],
[{"Field": "id", "Type": "int", "Null": "NO", "Key": "PRI", "Default": None, "Extra": "", "Comment": ""}],
)
# indexes
idx_result = _mock_query_result(
["Key_name", "Non_unique", "Index_type", "Column_name"],
[{"Key_name": "PRIMARY", "Non_unique": 0, "Index_type": "BTREE", "Column_name": "id"}],
)
# ddl
ddl_result = _mock_query_result(
["Table", "Create Table"],
[{"Table": "users", "Create Table": "CREATE TABLE users (id int)"}],
)
# row count
cnt_result = _mock_query_result(
["TABLE_ROWS"],
[{"TABLE_ROWS": 10}],
)
results = [tbl_result, col_result, idx_result, ddl_result, cnt_result]
call_count = 0
async def _mock_execute(*args, **kwargs):
nonlocal call_count
r = results[call_count]
call_count += 1
return r
with patch(
"app.services.dictionary_service.DBConnector.execute_query",
new=AsyncMock(side_effect=_mock_execute),
):
resp = await client.get(
"/api/v1/dictionary/instances/" + str(test_instance.id) + "/export",
params={"database": "test_db"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert "tables" in data
assert len(data["tables"]) == 1
assert data["tables"][0]["table_name"] == "users"
class TestPermission:
async def test_guest_forbidden(
self, client: AsyncClient, test_instance, guest_token
):
resp = await client.get(
"/api/v1/dictionary/instances/" + str(test_instance.id) + "/databases",
headers=auth_header(guest_token),
)
assert resp.status_code == 403
async def test_developer_allowed(
self, client: AsyncClient, test_instance, developer_token
):
mock_result = _mock_query_result(["Database"], [])
with patch(
"app.services.dictionary_service.DBConnector.execute_query",
new_callable=AsyncMock,
return_value=mock_result,
):
resp = await client.get(
"/api/v1/dictionary/instances/" + str(test_instance.id) + "/databases",
headers=auth_header(developer_token),
)
assert resp.status_code == 200