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,138 @@
|
||||
"""Tests for AES encryption (Phase 3.1)."""
|
||||
import pytest
|
||||
from app.core.crypto import encrypt_password, decrypt_password
|
||||
|
||||
|
||||
class TestEncryption:
|
||||
def test_encrypt_decrypt_roundtrip(self):
|
||||
"""Encrypt then decrypt returns original."""
|
||||
plain = "my_secret_password_123"
|
||||
encrypted = encrypt_password(plain)
|
||||
assert encrypted != plain
|
||||
assert decrypt_password(encrypted) == plain
|
||||
|
||||
def test_encrypt_produces_different_ciphertext(self):
|
||||
"""Each encryption uses a random nonce, so ciphertext differs."""
|
||||
plain = "same_password"
|
||||
e1 = encrypt_password(plain)
|
||||
e2 = encrypt_password(plain)
|
||||
assert e1 != e2 # different nonces
|
||||
assert decrypt_password(e1) == plain
|
||||
assert decrypt_password(e2) == plain
|
||||
|
||||
def test_encrypt_empty_string(self):
|
||||
"""Can encrypt and decrypt empty string."""
|
||||
encrypted = encrypt_password("")
|
||||
assert decrypt_password(encrypted) == ""
|
||||
|
||||
def test_encrypt_special_characters(self):
|
||||
"""Handles special characters in passwords."""
|
||||
plain = 'p@ssw0rd!#$%^&*()_+-=[]{}|;\':",./<>?'
|
||||
encrypted = encrypt_password(plain)
|
||||
assert decrypt_password(encrypted) == plain
|
||||
|
||||
def test_encrypt_unicode(self):
|
||||
"""Handles unicode characters."""
|
||||
plain = "密码测试🔑"
|
||||
encrypted = encrypt_password(plain)
|
||||
assert decrypt_password(encrypted) == plain
|
||||
|
||||
def test_decrypt_invalid_data_returns_as_is(self):
|
||||
"""Invalid ciphertext returns original string (backward compat)."""
|
||||
result = decrypt_password("not_valid_base64!!!")
|
||||
assert result == "not_valid_base64!!!"
|
||||
|
||||
|
||||
class TestInstanceEncryption:
|
||||
"""Test that instance passwords are stored encrypted via API."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_instance_encrypts_password(self, client, admin_token):
|
||||
"""POST /instances stores password as encrypted ciphertext."""
|
||||
from tests.conftest import auth_header
|
||||
resp = await client.post(
|
||||
"/api/v1/instances",
|
||||
json={
|
||||
"name": "加密测试",
|
||||
"db_type": "mysql",
|
||||
"host": "10.0.0.1",
|
||||
"port": 3306,
|
||||
"username": "root",
|
||||
"password": "plain_text_pw",
|
||||
"database": "test",
|
||||
"env": "development",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
data = resp.json()
|
||||
# Response should NOT contain password at all (InstanceResponse excludes it)
|
||||
assert "password" not in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_not_stored_as_plaintext(self, client, db, admin_token):
|
||||
"""Verify the database actually stores encrypted, not plain text."""
|
||||
from tests.conftest import auth_header
|
||||
from app.models.instance import Instance
|
||||
from sqlalchemy import select
|
||||
|
||||
plain_pw = "super_secret_123"
|
||||
resp = await client.post(
|
||||
"/api/v1/instances",
|
||||
json={
|
||||
"name": "明文检查",
|
||||
"db_type": "mysql",
|
||||
"host": "10.0.0.2",
|
||||
"port": 3306,
|
||||
"username": "root",
|
||||
"password": plain_pw,
|
||||
"database": "test",
|
||||
"env": "development",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
instance_id = resp.json()["id"]
|
||||
|
||||
# Read raw from database
|
||||
result = await db.execute(select(Instance).where(Instance.id == instance_id))
|
||||
instance = result.scalar_one()
|
||||
assert instance.password != plain_pw # Not stored as plaintext
|
||||
assert len(instance.password) > 20 # Encrypted is longer
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_instance_re_encrypts_password(self, client, db, admin_token):
|
||||
"""PUT /instances/:id re-encrypts password when updated."""
|
||||
from tests.conftest import auth_header
|
||||
from app.models.instance import Instance
|
||||
from sqlalchemy import select
|
||||
|
||||
# Create
|
||||
resp = await client.post(
|
||||
"/api/v1/instances",
|
||||
json={
|
||||
"name": "更新加密",
|
||||
"db_type": "mysql",
|
||||
"host": "10.0.0.3",
|
||||
"port": 3306,
|
||||
"username": "root",
|
||||
"password": "old_password",
|
||||
"database": "test",
|
||||
},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
iid = resp.json()["id"]
|
||||
|
||||
# Update password
|
||||
resp = await client.put(
|
||||
f"/api/v1/instances/{iid}",
|
||||
json={"password": "new_password"},
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Verify encrypted
|
||||
result = await db.execute(select(Instance).where(Instance.id == iid))
|
||||
inst = result.scalar_one()
|
||||
assert inst.password != "new_password"
|
||||
assert inst.password != "old_password"
|
||||
Reference in new issue
Block a user