From 730f0ba7caf1965063ab66971a4c082a050d8694 Mon Sep 17 00:00:00 2001 From: max Date: Wed, 30 Sep 2026 17:33:50 +0200 Subject: [PATCH] feat(ui,api): clean DB tab header, gray out enterprise features when unlicensed, enforce strict license gating on API --- src/Server.py | 413 ++++++++++++++++++++++--------------------- tests/test_server.py | 61 +++++-- 2 files changed, 263 insertions(+), 211 deletions(-) diff --git a/src/Server.py b/src/Server.py index 4f0a2ba..2c87259 100644 --- a/src/Server.py +++ b/src/Server.py @@ -3073,7 +3073,9 @@ def api_db_flush_buffer(): @app.post("/api/db/test-connection", dependencies=[Depends(require_admin_auth)]) def api_db_test_connection(req: DbConnectionTestRequest): """Tests connection to external PostgreSQL database. Requires enterprise_db license feature.""" - if not is_feature_licensed("enterprise_db"): + config = SERVER_STATE.get("config", {}) + db_path = config.get("db_path", DEFAULT_DB_FILE) + if not is_feature_licensed("enterprise_db", db_path): raise HTTPException( status_code=403, detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature." @@ -3087,12 +3089,13 @@ def api_db_test_connection(req: DbConnectionTestRequest): @app.post("/api/db/switch-backend", dependencies=[Depends(require_admin_auth)]) def api_db_switch_backend(req: DbSwitchBackendRequest): """Switches active database backend between SQLite and PostgreSQL. Requires enterprise_db license feature.""" - if not is_feature_licensed("enterprise_db"): + config = SERVER_STATE.get("config", {}) + db_path = config.get("db_path", DEFAULT_DB_FILE) + if not is_feature_licensed("enterprise_db", db_path): raise HTTPException( status_code=403, detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature." ) - config = SERVER_STATE.get("config", {}) if req.db_type == "postgres": pg_cfg = { "host": req.host or "localhost", @@ -3130,13 +3133,30 @@ def api_db_switch_backend(req: DbSwitchBackendRequest): @app.post("/api/db/config", dependencies=[Depends(require_admin_auth)]) def api_db_update_config(req: DbFullConfigRequest): """Updates complete database storage engine, cache mode, PostgreSQL parameters, and encryption at rest.""" - if req.storage_engine == "postgres" and not is_feature_licensed("enterprise_db"): - raise HTTPException( - status_code=403, - detail="External PostgreSQL storage is locked. This capability requires the 'enterprise_db' license feature." - ) - config = SERVER_STATE.get("config", {}) + db_path = config.get("db_path", DEFAULT_DB_FILE) + is_unlocked = is_feature_licensed("enterprise_db", db_path) + + if not is_unlocked: + if req.storage_engine == "postgres": + raise HTTPException( + status_code=403, + detail="External PostgreSQL storage is locked. This capability requires the 'enterprise_db' license feature." + ) + if req.cache_mode == "buffered": + raise HTTPException( + status_code=403, + detail="In-memory ingestion buffering is locked. This capability requires the 'enterprise_db' license feature." + ) + if req.encrypt_at_rest: + raise HTTPException( + status_code=403, + detail="Data-at-rest OpenPGP encryption is locked. This capability requires the 'enterprise_db' license feature." + ) + req.storage_engine = "sqlite" + req.cache_mode = "direct" + req.encrypt_at_rest = False + config["storage_engine"] = req.storage_engine config["db_type"] = req.storage_engine config["cache_mode"] = req.cache_mode @@ -3149,7 +3169,7 @@ def api_db_update_config(req: DbFullConfigRequest): config["batch_size"] = INGEST_QUEUE.batch_size config["max_queue"] = INGEST_QUEUE.max_queue - if req.pg_config: + if req.pg_config and is_unlocked: pg_dict = req.pg_config.model_dump() config["postgres"] = pg_dict if req.storage_engine == "postgres": @@ -3175,12 +3195,13 @@ def api_db_update_config(req: DbFullConfigRequest): @app.post("/api/db/migrate-to-postgres", dependencies=[Depends(require_admin_auth)]) def api_db_migrate_to_postgres(req: DbConnectionTestRequest): """Migrates all telemetry, runs, clients, users, and license configs from SQLite to PostgreSQL.""" - if not is_feature_licensed("enterprise_db"): + config = SERVER_STATE.get("config", {}) + db_path = config.get("db_path", DEFAULT_DB_FILE) + if not is_feature_licensed("enterprise_db", db_path): raise HTTPException( status_code=403, detail="Enterprise Database feature is locked. This capability requires a valid license with the 'enterprise_db' feature." ) - config = SERVER_STATE.get("config", {}) sqlite_path = config.get("db_path", DEFAULT_DB_FILE) INGEST_QUEUE.flush(db_path=sqlite_path) @@ -3217,8 +3238,6 @@ def serve_webui(request: Request = None): queue_stats = INGEST_QUEUE.get_stats() storage_info = get_storage_engine_info() pg_config = config.get("postgres", {}) - db_tab_btn_badge = '🟢 UNLOCKED' if is_db_unlocked else '🔒 ENTERPRISE' - # Fetch clients conn = sqlite3.connect(db_path) @@ -3363,205 +3382,203 @@ def serve_webui(request: Request = None): if detected_host in ("0.0.0.0", "localhost", ""): detected_host = socket.gethostname() - is_encrypt_at_rest = config.get("encrypt_at_rest", False) + is_encrypt_at_rest = config.get("encrypt_at_rest", False) and is_db_unlocked encrypt_status_label = "🔒 OpenPGP Encrypted" if is_encrypt_at_rest else "🔓 Plaintext Storage" encrypt_status_color = "#34d399" if is_encrypt_at_rest else "#94a3b8" - if is_db_unlocked: - db_tab_pane_html = f""" - -
- -
-
-
- 💾 Storage Engine & Live Telemetry Metrics -
Centralized high-availability SQL pooling, buffer telemetry, and encryption at rest
-
- {storage_info['engine'].upper()} BACKEND -
-
-
-
Active Storage Backend
-
{storage_info['backend_display']}
-
{storage_info['status']}
-
-
-
Telemetry Records
-
{storage_info['table_stats']['active_issues']} Issues / {storage_info['table_stats']['ingest_runs']} Runs
-
Clients: {storage_info['table_stats']['clients']} | Users: {storage_info['table_stats']['users']}
-
-
-
Buffered Events in RAM
-
{queue_stats['queue_size']} events
-
Ingested: {queue_stats['total_ingested']} | Flushed: {queue_stats['total_flushed']}
-
-
-
Data-at-Rest Security
-
{encrypt_status_label}
-
Zero-Knowledge Storage on External DB
-
+ # Grayed-out styling & attributes for unlicensed enterprise features + ent_badge = 'ENTERPRISE LICENSE REQUIRED' + buf_disabled_attr = "" if is_db_unlocked else "disabled" + buf_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;" + enc_disabled_attr = "" if is_db_unlocked else "disabled" + enc_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;" + pg_disabled_attr = "" if is_db_unlocked else "disabled" + pg_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;" + + ent_community_notice = "" if is_db_unlocked else f""" +
+
+ 💡 Standard Local Storage: Embedded SQLite 3 storage with synchronous logging is active. High-throughput in-memory buffering, zero-knowledge OpenPGP at-rest encryption, and external PostgreSQL clustering require an enterprise_db license capability. +
+ +
+ """ + + db_tab_pane_html = f""" + +
+ +
+
+
+ 💾 Storage Engine & Live Telemetry Metrics +
Centralized high-availability SQL pooling, buffer telemetry, and encryption at rest
+ {storage_info['engine'].upper()} BACKEND
- - -
-
- Database Storage Engine Configuration - CONFIG & TUNING +
+
+
Active Storage Backend
+
{storage_info['backend_display']}
+
{storage_info['status']}
- - -
-
- ⚡ Ingestion Cache & Buffer Mode - -
- -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
+
+
Telemetry Records
+
{storage_info['table_stats']['active_issues']} Issues / {storage_info['table_stats']['ingest_runs']} Runs
+
Clients: {storage_info['table_stats']['clients']} | Users: {storage_info['table_stats']['users']}
- - -
-
- 🔐 Zero-Knowledge Data Encryption at Rest -
- -

- When enabled, anomaly message payloads stored in the external database are encrypted with the server's OpenPGP public key before writing. Only this hub instance can decrypt records for authorized WebUI and Hermes AI usage, ensuring zero-knowledge privacy on remote database servers. -

+
+
Buffered Events in RAM
+
{queue_stats['queue_size']} events
+
Ingested: {queue_stats['total_ingested']} | Flushed: {queue_stats['total_flushed']}
- - -
-
- 🗄️ Select Storage Engine Backend -
-
- - -
- - -
-
- 🐘 External PostgreSQL Connection Parameters - ENTERPRISE POOL -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
- - -
- +
+
Data-at-Rest Security
+
{encrypt_status_label}
+
{'Zero-Knowledge Storage on External DB' if is_encrypt_at_rest else 'Local Encrypted/Plaintext Storage'}
- """ - else: - db_tab_pane_html = f""" - -
-
-
🔒
-

Enterprise Database & High-Throughput Engine is Locked

-
- - REQUIRES 'enterprise_db' LICENSE FEATURE - + + +
+
+ Database Storage Engine Configuration + CONFIG & TUNING +
+ + {ent_community_notice} + + +
+
+ ⚡ Ingestion Cache & Buffer Mode +
-

- Direct PostgreSQL storage backend clustering, automatic schema synchronization, and customizable high-throughput batch-flush controls are gated behind the enterprise_db capability. + +

+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ 🔐 Zero-Knowledge Data Encryption at Rest +
+ +

+ When enabled, anomaly message payloads stored in the external database are encrypted with the server's OpenPGP public key before writing. Only this hub instance can decrypt records for authorized WebUI and Hermes AI usage, ensuring zero-knowledge privacy on remote database servers.

-
- - +
+ + +
+
+ 🗄️ Select Storage Engine Backend +
+
+ + +
+ + +
+
+ 🐘 External PostgreSQL Connection Parameters + ENTERPRISE POOL +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ +
- """ +
+ """ html_content = f""" @@ -3793,7 +3810,7 @@ def serve_webui(request: Request = None): - +
diff --git a/tests/test_server.py b/tests/test_server.py index c9995a0..5da9457 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -710,7 +710,53 @@ class TestServerComponent(unittest.TestCase): 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) + # 2. Test that without enterprise_db license, buffering, encryption at rest, and postgres are blocked + cfg_unlicensed_buf = Server.DbFullConfigRequest( + storage_engine="sqlite", + cache_mode="buffered", + encrypt_at_rest=False + ) + with self.assertRaises(Server.HTTPException) as cm: + Server.api_db_update_config(cfg_unlicensed_buf) + self.assertEqual(cm.exception.status_code, 403) + self.assertIn("enterprise_db", cm.exception.detail) + + cfg_unlicensed_enc = Server.DbFullConfigRequest( + storage_engine="sqlite", + cache_mode="direct", + encrypt_at_rest=True + ) + with self.assertRaises(Server.HTTPException) as cm: + Server.api_db_update_config(cfg_unlicensed_enc) + self.assertEqual(cm.exception.status_code, 403) + self.assertIn("enterprise_db", cm.exception.detail) + + 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) + + # 3. Standard local SQLite configuration succeeds when unlicensed + cfg_standard = Server.DbFullConfigRequest( + storage_engine="sqlite", + cache_mode="direct", + encrypt_at_rest=False + ) + res_std = Server.api_db_update_config(cfg_standard) + self.assertEqual(res_std["status"], "success") + + # 4. Unlock enterprise_db feature in license_config + conn = sqlite3.connect(self.test_db) + conn.execute("UPDATE license_config SET features = ? WHERE id = 1", (json.dumps(["enterprise_db"]),)) + conn.commit() + conn.close() + + # 5. Test updating config with enterprise_db unlocked (direct -> buffered, encrypt_at_rest = True) cfg_req = Server.DbFullConfigRequest( storage_engine="sqlite", cache_mode="buffered", @@ -726,18 +772,7 @@ class TestServerComponent(unittest.TestCase): 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 + # 6. 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"])