Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa662da476 | ||
|
|
6cb9631958 | ||
|
|
c03bb348b5 | ||
|
|
2d39f3c007 | ||
|
|
78e62b31d2 | ||
|
|
860e39b360 | ||
|
|
2a04244db3 | ||
|
|
9f999fc6c3 | ||
|
|
3399c6d57b | ||
|
|
c6d04f8896 | ||
|
|
0a67b43619 |
+4
-3
@@ -22,14 +22,15 @@ out/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
client_state.json
|
||||
*.tmp
|
||||
|
||||
# Live configuration with generated private keys & tokens (samples are tracked)
|
||||
server_config.json
|
||||
client_config.json
|
||||
client_state.json
|
||||
local_collector_state.json
|
||||
test_*.json
|
||||
test_*.db
|
||||
test_pipeline_master_ca/
|
||||
test_pipeline_certs/
|
||||
master_ca_certs/
|
||||
.env
|
||||
*.env
|
||||
|
||||
+1
-1
Submodule LOGAR-LICENSING updated: 7b43a4c82d...38a41cfbac
@@ -5,10 +5,9 @@
|
||||
# LOGAR: Fleet Log Aggregation & Centralized Provisioning System
|
||||
### *Secure Log Aggregation, PKI Fleet Licensing & Centralized Delivery for Autonomous AI Analysis*
|
||||
|
||||
[](RELEASE_NOTES.md)
|
||||
[](tests/)
|
||||
[](RELEASE_NOTES.md)
|
||||
[](tests/)
|
||||
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||
[](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||
|
||||
**LOGAR** is an enterprise-grade log aggregation, normalization, and centralized provisioning system built for heterogeneous server fleets (Windows & Linux). It pairs ultra-lightweight, zero-state edge forwarders with a secure centralized server hub that enforces mutual TLS 1.3 (**mTLS**) authentication, dynamic PKI fleet licensing with cryptographic seat leasing, 12-hour temporal persistence tracking, and direct anomaly pass-through—delivering clean, structured, verified log telemetry directly to **Hermes AI** for autonomous deep reasoning, contextual root-cause analysis, and operational decision-making.
|
||||
|
||||
@@ -174,10 +173,10 @@ python compilation/package_dist.py --target all
|
||||
|
||||
For in-depth operational guides, lifecycle state machines, and PKI specifications, consult the **[LOGAR Gitea Wiki](https://gitea.eibl.tech/me0nline/LOGAR/wiki)**:
|
||||
|
||||
* [Branding & Design System Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||
* [Architecture & Design Details](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Architecture-and-Design)
|
||||
* [System State Machines & Subsystem Lifecycles](https://gitea.eibl.tech/me0nline/LOGAR/wiki/State-Machines-and-Lifecycles)
|
||||
* [PKI Architecture & Custom X.509 Seat OIDs](https://gitea.eibl.tech/me0nline/LOGAR/wiki/PKI-and-Licensing)
|
||||
* [Complete REST API Specification](https://gitea.eibl.tech/me0nline/LOGAR/wiki/API-Reference)
|
||||
* [Production Deployment & Service Management](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Deployment-and-Installation)
|
||||
* [CI/CD Pipeline & Cross-Compilation Workflow](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Testing-and-Releases)
|
||||
* [Branding & Design System Guide](https://gitea.eibl.tech/me0nline/LOGAR/wiki/Branding-Guide)
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# LOGAR Release Notes
|
||||
|
||||
## [v0.3.4.2] - 2026-09-30
|
||||
|
||||
### 🔒 Exclusive Hardware Host-Binding & Reassignment Controls
|
||||
- **Automated Host Binding on First Activation**: Licenses now automatically lock to the specific server hardware fingerprint (`server_fingerprint`) and hostname upon first activation/enrollment, preventing multi-host cloning or license hijacking.
|
||||
- **Manual Host Reassignment Control**: Added `🔓 Reset Host Binding` button to the Master License Authority dashboard (`licenseDetailsModal`), allowing administrators to unbind a license so it can be cleanly transferred or reassigned to a new server instance.
|
||||
- **REST & CLI Management**: Added `POST /api/license/reset-binding` and CLI argument `--reset-binding LIC_ID` for automated provisioning pipelines.
|
||||
|
||||
---
|
||||
|
||||
## [v0.3.4.1] - 2026-09-27
|
||||
|
||||
### 🎨 Installer Dialog UI Spacing & Branding Enhancements
|
||||
- **NSIS Client Config Profile Dialog Fix**: Adjusted vertical coordinates and label boundaries in the Windows Client NSIS installer (`installer_client.nsi`) to prevent input field overlap and ensure multi-line instructional text renders without visual clipping.
|
||||
- **Installer Footer Branding Clean-Up**: Escaped ampersands in Windows installer branding labels (`BrandingText`) to avoid character truncation in native win32 footer panels.
|
||||
- **License Authority UI Clean-Up**: Simplified table ACTIONS column in the Licensing Authority portal to exclusively display `⚙️ Manage`, keeping secondary actions (`Reinstate`, `Delete`) inside the dedicated management modal.
|
||||
|
||||
---
|
||||
|
||||
## [v0.3.4] - 2026-09-27
|
||||
|
||||
### 🔄 In-Place Installer Upgrades & Mutual Exclusion Guards
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Setup]
|
||||
AppId={{9C4A6F1D-8975-476E-8512-4217DE029A88}}
|
||||
AppName=LOGAR Client
|
||||
AppVersion=0.3.4
|
||||
AppVersion=0.3.4.2
|
||||
AppPublisher=LOGAR
|
||||
AppPublisherURL=https://logar.eibl.tech
|
||||
AppSupportURL=https://logar.eibl.tech
|
||||
@@ -19,9 +19,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
|
||||
WizardSmallImageFile=assets\wizard_small.bmp
|
||||
VersionInfoCompany=LOGAR
|
||||
VersionInfoDescription=LOGAR Windows Edge Log Forwarder Setup
|
||||
VersionInfoVersion=0.3.4.0
|
||||
VersionInfoVersion=0.3.4.2
|
||||
VersionInfoProductName=LOGAR Client
|
||||
VersionInfoProductVersion=0.3.4
|
||||
VersionInfoProductVersion=0.3.4.2
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\Win_Client.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
@@ -7,15 +7,15 @@ OutFile "../dist/LOGAR-Client-Setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\LOGAR"
|
||||
RequestExecutionLevel admin
|
||||
|
||||
BrandingText "LOGAR Centralized Log Aggregator & Forwarder"
|
||||
BrandingText "LOGAR Centralized Log Aggregator && Forwarder"
|
||||
|
||||
VIProductVersion "0.3.4.0"
|
||||
VIProductVersion "0.3.4.2"
|
||||
VIAddVersionKey "ProductName" "LOGAR Client"
|
||||
VIAddVersionKey "CompanyName" "LOGAR"
|
||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
||||
VIAddVersionKey "FileDescription" "LOGAR Windows Edge Log Forwarder Setup"
|
||||
VIAddVersionKey "FileVersion" "0.3.4.0"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.0"
|
||||
VIAddVersionKey "FileVersion" "0.3.4.2"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.2"
|
||||
|
||||
# --- Visual Graphics & Branding ---
|
||||
!define MUI_ICON "assets/logo.ico"
|
||||
@@ -127,13 +127,13 @@ Function ConfigPageCreate
|
||||
|
||||
!insertmacro MUI_HEADER_TEXT "Import Client Configuration (Optional)" "Choose a client_config.json profile exported from LOGAR Server."
|
||||
|
||||
${NSD_CreateLabel} 0 0 100% 30u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
|
||||
${NSD_CreateLabel} 0 0 100% 48u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
|
||||
Pop $0
|
||||
|
||||
${NSD_CreateText} 0 38u 75% 12u "$CFGFILE"
|
||||
${NSD_CreateText} 0 54u 75% 14u "$CFGFILE"
|
||||
Pop $CfgFileInput
|
||||
|
||||
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
|
||||
${NSD_CreateButton} 78% 53u 22% 16u "Browse..."
|
||||
Pop $CfgBrowseBtn
|
||||
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Setup]
|
||||
AppId={{4F3D675E-1E4E-43B6-8C84-B47DE290BE4A}}
|
||||
AppName=LOGAR Server
|
||||
AppVersion=0.3.4
|
||||
AppVersion=0.3.4.2
|
||||
AppPublisher=LOGAR
|
||||
AppPublisherURL=https://logar.eibl.tech
|
||||
AppSupportURL=https://logar.eibl.tech
|
||||
@@ -19,9 +19,9 @@ WizardImageFile=assets\wizard_sidebar.bmp
|
||||
WizardSmallImageFile=assets\wizard_small.bmp
|
||||
VersionInfoCompany=LOGAR
|
||||
VersionInfoDescription=LOGAR Cloud Hub & TCP Ingestion Server Setup
|
||||
VersionInfoVersion=0.3.4.0
|
||||
VersionInfoVersion=0.3.4.2
|
||||
VersionInfoProductName=LOGAR Server
|
||||
VersionInfoProductVersion=0.3.4
|
||||
VersionInfoProductVersion=0.3.4.2
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\Server.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
@@ -6,15 +6,15 @@ OutFile "../dist/LOGAR-Server-Setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\LOGAR-Server"
|
||||
RequestExecutionLevel admin
|
||||
|
||||
BrandingText "LOGAR Centralized Log Aggregator & Ingestion Hub"
|
||||
BrandingText "LOGAR Centralized Log Aggregator && Ingestion Hub"
|
||||
|
||||
VIProductVersion "0.3.4.0"
|
||||
VIProductVersion "0.3.4.2"
|
||||
VIAddVersionKey "ProductName" "LOGAR Server"
|
||||
VIAddVersionKey "CompanyName" "LOGAR"
|
||||
VIAddVersionKey "LegalCopyright" "© 2026 LOGAR"
|
||||
VIAddVersionKey "FileDescription" "LOGAR Cloud Hub & TCP Ingestion Server Setup"
|
||||
VIAddVersionKey "FileVersion" "0.3.4.0"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.0"
|
||||
VIAddVersionKey "FileVersion" "0.3.4.2"
|
||||
VIAddVersionKey "ProductVersion" "0.3.4.2"
|
||||
|
||||
# --- Visual Graphics & Branding ---
|
||||
!define MUI_ICON "assets/logo.ico"
|
||||
|
||||
+1322
-48
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
OID_LOGAR_SEATS = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.1")
|
||||
OID_LOGAR_LICENSE_ID = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.2")
|
||||
OID_LOGAR_ISSUER_URL = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.3")
|
||||
OID_LOGAR_FEATURES = x509.ObjectIdentifier("1.3.6.1.4.1.59999.1.4")
|
||||
|
||||
|
||||
|
||||
@@ -448,6 +449,19 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
|
||||
except (x509.ExtensionNotFound, Exception):
|
||||
issuer_url = None
|
||||
|
||||
features = []
|
||||
try:
|
||||
ext_feat = cert.extensions.get_extension_for_oid(OID_LOGAR_FEATURES)
|
||||
if ext_feat and ext_feat.value:
|
||||
raw_val = ext_feat.value.value if hasattr(ext_feat.value, "value") else str(ext_feat.value)
|
||||
feat_str = raw_val.decode("utf-8").strip() if isinstance(raw_val, bytes) else str(raw_val).strip()
|
||||
if feat_str.startswith("["):
|
||||
features = json.loads(feat_str)
|
||||
else:
|
||||
features = [f.strip() for f in feat_str.split(",") if f.strip()]
|
||||
except Exception:
|
||||
features = []
|
||||
|
||||
not_before = getattr(cert, "not_valid_before_utc", None) or cert.not_valid_before.replace(tzinfo=datetime.timezone.utc)
|
||||
not_after = getattr(cert, "not_valid_after_utc", None) or cert.not_valid_after.replace(tzinfo=datetime.timezone.utc)
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
@@ -482,6 +496,7 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
|
||||
"seats": seats,
|
||||
"license_id": license_id,
|
||||
"issuer_url": issuer_url,
|
||||
"features": features,
|
||||
"subject_cn": subject_cn,
|
||||
"issuer_cn": issuer_cn,
|
||||
"is_ca": is_ca,
|
||||
@@ -603,11 +618,12 @@ def sign_ca_csr_with_seats(
|
||||
seats: int,
|
||||
days_valid: int = 365,
|
||||
license_id: Optional[str] = None,
|
||||
license_server_url: str = "https://logar.eibl.tech"
|
||||
license_server_url: str = "https://logar.eibl.tech",
|
||||
features: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
Signs a subordinate CA CSR using the master license authority Root CA.
|
||||
Embeds the approved seat count and license ID via custom X.509 extensions.
|
||||
Embeds the approved seat count, license ID, and optional enterprise features via custom X.509 extensions.
|
||||
Returns the signed PEM certificate.
|
||||
"""
|
||||
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
|
||||
@@ -666,6 +682,13 @@ def sign_ca_csr_with_seats(
|
||||
)
|
||||
)
|
||||
|
||||
if features:
|
||||
feat_str = json.dumps(features) if isinstance(features, list) else str(features)
|
||||
builder = builder.add_extension(
|
||||
x509.UnrecognizedExtension(OID_LOGAR_FEATURES, feat_str.encode("utf-8")),
|
||||
critical=False
|
||||
)
|
||||
|
||||
signed_cert = builder.sign(master_ca_key, hashes.SHA256())
|
||||
return signed_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
|
||||
|
||||
@@ -903,6 +926,7 @@ def import_license_package(
|
||||
"master_root_ca_pem": master_root_ca_pem,
|
||||
"license_info": info,
|
||||
"seats": info.get("seats"),
|
||||
"features": info.get("features", []),
|
||||
"license_id": info.get("license_id"),
|
||||
"issuer_url": info.get("issuer_url"),
|
||||
"fingerprint": info.get("fingerprint"),
|
||||
|
||||
@@ -510,8 +510,242 @@ class TestServerComponent(unittest.TestCase):
|
||||
info_disabled = Server.get_user_totp_info("admin", db_path=self.test_db)
|
||||
self.assertFalse(info_disabled["enabled"])
|
||||
|
||||
def test_persistent_session_secret_and_rate_limiting(self):
|
||||
"""Tests DB persistence of session secret and sliding window rate limiting."""
|
||||
Server.init_db(self.test_db)
|
||||
|
||||
# 1. Session secret persistence
|
||||
sec1 = Server.get_session_secret(self.test_db)
|
||||
self.assertTrue(len(sec1) >= 32)
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT value FROM server_meta WHERE key = 'session_secret'")
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(row[0], sec1)
|
||||
|
||||
# Token generated with test_db
|
||||
token = Server.generate_session_token("admin", db_path=self.test_db)
|
||||
self.assertEqual(Server.verify_session_token(token, db_path=self.test_db), "admin")
|
||||
|
||||
# 2. Rate limiting
|
||||
test_ip = "10.0.0.99"
|
||||
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
|
||||
|
||||
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
||||
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
||||
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||
|
||||
Server.record_failed_ip_attempt(test_ip, scope="login", db_path=self.test_db)
|
||||
self.assertFalse(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||
|
||||
# Different scope ('enroll') should still be allowed
|
||||
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="enroll", db_path=self.test_db))
|
||||
|
||||
Server.reset_failed_ip_attempts(test_ip, scope="login", db_path=self.test_db)
|
||||
self.assertTrue(Server.check_ip_rate_limit(test_ip, max_attempts=3, window_seconds=60, scope="login", db_path=self.test_db))
|
||||
|
||||
def test_ingest_queue_manager_and_batch_flush(self):
|
||||
"""Tests IngestQueueManager buffering, capacity bounds, and atomic batch flushing."""
|
||||
Server.init_db(self.test_db)
|
||||
qm = Server.IngestQueueManager(flush_interval=1.0, batch_size=5, max_queue=50)
|
||||
|
||||
# Initial stats
|
||||
stats = qm.get_stats()
|
||||
self.assertEqual(stats["queue_size"], 0)
|
||||
self.assertEqual(stats["total_ingested"], 0)
|
||||
self.assertEqual(stats["total_flushed"], 0)
|
||||
|
||||
# Push 3 payloads
|
||||
for i in range(3):
|
||||
payload = {
|
||||
"server": f"node-{i}.corp.local",
|
||||
"logs": [
|
||||
{"server": f"node-{i}", "signature": f"Sig_{i}", "severity": "WARNING", "message": f"Log msg {i}", "os_type": "linux"}
|
||||
]
|
||||
}
|
||||
pushed = qm.push(payload)
|
||||
self.assertTrue(pushed)
|
||||
|
||||
stats = qm.get_stats()
|
||||
self.assertEqual(stats["queue_size"], 3)
|
||||
self.assertEqual(stats["total_ingested"], 3)
|
||||
|
||||
# Flush to database
|
||||
res_flush = qm.flush(db_path=self.test_db, window_hours=12, min_runs=1)
|
||||
self.assertEqual(res_flush["flushed_runs"], 3)
|
||||
self.assertEqual(res_flush["processed_logs"], 3)
|
||||
|
||||
# Verify DB records
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT COUNT(*) FROM ingest_runs")
|
||||
run_count = c.fetchone()[0]
|
||||
c.execute("SELECT COUNT(*) FROM active_issues WHERE status = 'VERIFIED'")
|
||||
issue_count = c.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
self.assertEqual(run_count, 3)
|
||||
self.assertEqual(issue_count, 3)
|
||||
|
||||
# Stats after flush
|
||||
stats = qm.get_stats()
|
||||
self.assertEqual(stats["queue_size"], 0)
|
||||
self.assertEqual(stats["total_flushed"], 3)
|
||||
|
||||
def test_enterprise_db_feature_gating(self):
|
||||
"""Tests feature check helpers and DB storage info under standard and enterprise licensed states."""
|
||||
Server.init_db(self.test_db)
|
||||
|
||||
# 1. Standard / Unlicensed state
|
||||
self.assertFalse(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
||||
info = Server.get_storage_engine_info(db_path=self.test_db)
|
||||
self.assertEqual(info["engine"], "sqlite")
|
||||
self.assertFalse(info["feature_unlocked"])
|
||||
self.assertIn("SQLite 3 (WAL Mode)", info["backend_display"])
|
||||
|
||||
# 2. Unlock enterprise_db feature in DB
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db", "ai_heuristics"]),))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
self.assertTrue(Server.is_feature_licensed("enterprise_db", db_path=self.test_db))
|
||||
self.assertTrue(Server.is_feature_licensed("ai_heuristics", db_path=self.test_db))
|
||||
self.assertFalse(Server.is_feature_licensed("unknown_feat", db_path=self.test_db))
|
||||
|
||||
info_unlocked = Server.get_storage_engine_info(db_path=self.test_db)
|
||||
self.assertTrue(info_unlocked["feature_unlocked"])
|
||||
self.assertIn("enterprise_db", info_unlocked["licensed_features"])
|
||||
|
||||
def test_postgres_storage_endpoints_authorization(self):
|
||||
"""Tests REST API security gating on DB engine management endpoints."""
|
||||
Server.init_db(self.test_db)
|
||||
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "storage_engine": "sqlite"}
|
||||
|
||||
# 1. Test direct authorization & endpoint logic
|
||||
stats = Server.api_db_stats()
|
||||
self.assertIn("queue", stats)
|
||||
self.assertIn("storage", stats)
|
||||
self.assertFalse(stats["enterprise_db_licensed"])
|
||||
|
||||
# 2. Test Connection without enterprise_db -> HTTPException 403
|
||||
pg_req = Server.DbConnectionTestRequest(host="localhost", port=5432, database="logar", user="postgres", password="")
|
||||
with self.assertRaises(Server.HTTPException) as cm:
|
||||
Server.api_db_test_connection(pg_req)
|
||||
self.assertEqual(cm.exception.status_code, 403)
|
||||
self.assertIn("enterprise_db", cm.exception.detail)
|
||||
|
||||
# 3. Switch backend without enterprise_db -> HTTPException 403
|
||||
switch_req = Server.DbSwitchBackendRequest(db_type="postgres", host="localhost", port=5432, database="logar", user="postgres", password="")
|
||||
with self.assertRaises(Server.HTTPException) as cm:
|
||||
Server.api_db_switch_backend(switch_req)
|
||||
self.assertEqual(cm.exception.status_code, 403)
|
||||
self.assertIn("enterprise_db", cm.exception.detail)
|
||||
|
||||
# 4. Flush buffer endpoint executes successfully
|
||||
flush_res = Server.api_db_flush_buffer()
|
||||
self.assertEqual(flush_res["status"], "success")
|
||||
|
||||
# 5. Optional HTTP client verification if starlette testclient dependencies are present
|
||||
try:
|
||||
from fastapi.testclient import TestClient
|
||||
client = TestClient(Server.app)
|
||||
|
||||
# Unauthenticated request to /api/db/stats -> 401
|
||||
res = client.get("/api/db/stats")
|
||||
self.assertEqual(res.status_code, 401)
|
||||
|
||||
# Authenticated request with session cookie
|
||||
token = Server.generate_session_token("admin", db_path=self.test_db)
|
||||
client.cookies.set(Server.SESSION_COOKIE_NAME, token)
|
||||
|
||||
res_auth = client.get("/api/db/stats")
|
||||
self.assertEqual(res_auth.status_code, 200)
|
||||
data = res_auth.json()
|
||||
self.assertIn("queue", data)
|
||||
self.assertIn("storage", data)
|
||||
self.assertFalse(data["enterprise_db_licensed"])
|
||||
|
||||
# 403 checks via HTTP client
|
||||
res_test = client.post("/api/db/test-connection", json={"host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
|
||||
self.assertEqual(res_test.status_code, 403)
|
||||
|
||||
res_switch = client.post("/api/db/switch-backend", json={"db_type": "postgres", "host": "localhost", "port": 5432, "database": "logar", "user": "postgres", "password": ""})
|
||||
self.assertEqual(res_switch.status_code, 403)
|
||||
|
||||
res_flush = client.post("/api/db/flush-buffer")
|
||||
self.assertEqual(res_flush.status_code, 200)
|
||||
except (ImportError, RuntimeError, Exception):
|
||||
# Starlette TestClient may raise RuntimeError when httpx/httpx2 is missing in lightweight runner environments
|
||||
pass
|
||||
|
||||
def test_db_encryption_at_rest_and_full_config_api(self):
|
||||
"""Tests OpenPGP data-at-rest encryption/decryption and full DB/Cache configuration API."""
|
||||
Server.init_db(self.test_db)
|
||||
priv_armored, pub_armored, fp = Server.generate_server_keypair("test-db-node")
|
||||
priv_key, _ = pgpy.PGPKey.from_blob(priv_armored)
|
||||
pub_key, _ = pgpy.PGPKey.from_blob(pub_armored)
|
||||
|
||||
Server.SERVER_STATE["config"] = {
|
||||
"db_path": self.test_db,
|
||||
"storage_engine": "sqlite",
|
||||
"cache_mode": "direct",
|
||||
"encrypt_at_rest": False,
|
||||
"private_key": priv_armored,
|
||||
"public_key": pub_armored,
|
||||
"server_fingerprint": fp
|
||||
}
|
||||
Server.SERVER_STATE["private_key_obj"] = priv_key
|
||||
Server.SERVER_STATE["public_key_obj"] = pub_key
|
||||
|
||||
# 1. Test OpenPGP encryption & decryption roundtrip
|
||||
raw_msg = "Critical database corruption: segment 0x89FA damaged"
|
||||
encrypted_msg = Server.encrypt_field_for_storage(raw_msg, force=True)
|
||||
self.assertTrue(encrypted_msg.startswith("-----BEGIN PGP MESSAGE-----"))
|
||||
self.assertNotIn("Critical database corruption", encrypted_msg)
|
||||
|
||||
decrypted_msg = Server.decrypt_field_from_storage(encrypted_msg)
|
||||
self.assertEqual(decrypted_msg, raw_msg)
|
||||
|
||||
# 2. Test updating config via api_db_update_config (direct -> buffered, encrypt_at_rest = True)
|
||||
cfg_req = Server.DbFullConfigRequest(
|
||||
storage_engine="sqlite",
|
||||
cache_mode="buffered",
|
||||
flush_interval=4.0,
|
||||
batch_size=200,
|
||||
max_queue=25000,
|
||||
encrypt_at_rest=True
|
||||
)
|
||||
res = Server.api_db_update_config(cfg_req)
|
||||
self.assertEqual(res["status"], "success")
|
||||
self.assertEqual(Server.SERVER_STATE["config"]["cache_mode"], "buffered")
|
||||
self.assertTrue(Server.SERVER_STATE["config"]["encrypt_at_rest"])
|
||||
self.assertEqual(Server.INGEST_QUEUE.flush_interval, 4.0)
|
||||
self.assertEqual(Server.INGEST_QUEUE.batch_size, 200)
|
||||
|
||||
# 3. Test that switching to external PostgreSQL requires enterprise_db license
|
||||
pg_req = Server.DbFullConfigRequest(
|
||||
storage_engine="postgres",
|
||||
cache_mode="direct",
|
||||
pg_config=Server.DbConnectionTestRequest(host="db.corp.internal", port=5432, database="logar", user="postgres", password="")
|
||||
)
|
||||
with self.assertRaises(Server.HTTPException) as cm:
|
||||
Server.api_db_update_config(pg_req)
|
||||
self.assertEqual(cm.exception.status_code, 403)
|
||||
self.assertIn("enterprise_db", cm.exception.detail)
|
||||
|
||||
# 4. Verify storage engine info output
|
||||
info = Server.get_storage_engine_info(db_path=self.test_db)
|
||||
self.assertEqual(info["cache_mode"], "buffered")
|
||||
self.assertTrue(info["encrypt_at_rest"])
|
||||
self.assertEqual(info["flush_interval"], 4.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user