Refactor Server.py into modular domain classes (models, auth, storage, queue, licensing, PKI, UI)
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m14s

This commit is contained in:
2026-10-02 21:57:29 +02:00
parent 909dc640e3
commit f2a9e43619
8 changed files with 3751 additions and 3461 deletions
+473 -3455
View File
File diff suppressed because it is too large Load Diff
+398
View File
@@ -0,0 +1,398 @@
"""
LOGAR - Server Authentication & Access Control Managers
Provides modular classes for Session Management, Rate Limiting, Password Hashing,
Two-Factor Authentication (TOTP / RFC 6238), and Hermes API Tokens.
"""
import os
import time
import base64
import struct
import secrets
import hashlib
import hmac
import sqlite3
import urllib.parse
import threading
from datetime import datetime, timezone
from typing import Optional, Dict, Any, List, Tuple
from fastapi import Request, HTTPException
class SessionManager:
"""Manages cryptographic session secrets and HMAC-signed session tokens."""
SESSION_COOKIE_NAME = "logar_server_session"
_CACHED_SESSION_SECRETS: Dict[str, str] = {}
@classmethod
def get_session_secret(cls, db_path: Optional[str] = None, fallback_db: str = "logar_state.db") -> str:
"""Retrieves or persists a cryptographically secure session secret in the database."""
env_sec = os.environ.get("LOGAR_SESSION_SECRET")
if env_sec:
return env_sec
target_db = db_path or fallback_db
abs_db = os.path.abspath(target_db)
if abs_db in cls._CACHED_SESSION_SECRETS:
return cls._CACHED_SESSION_SECRETS[abs_db]
try:
conn = sqlite3.connect(target_db, timeout=5.0)
c = conn.cursor()
c.execute("CREATE TABLE IF NOT EXISTS server_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
c.execute("SELECT value FROM server_meta WHERE key = 'session_secret'")
row = c.fetchone()
if row and row[0]:
cls._CACHED_SESSION_SECRETS[abs_db] = row[0]
conn.close()
return row[0]
new_secret = secrets.token_hex(32)
c.execute("INSERT OR REPLACE INTO server_meta (key, value) VALUES ('session_secret', ?)", (new_secret,))
conn.commit()
conn.close()
cls._CACHED_SESSION_SECRETS[abs_db] = new_secret
return new_secret
except Exception:
fallback = secrets.token_hex(32)
cls._CACHED_SESSION_SECRETS[abs_db] = fallback
return fallback
@classmethod
def generate_session_token(cls, username: str, db_path: Optional[str] = None, fallback_db: str = "logar_state.db") -> str:
"""Generates an HMAC-signed session cookie string."""
ts = int(datetime.now(timezone.utc).timestamp())
nonce = secrets.token_hex(8)
payload = f"{username}:{ts}:{nonce}"
secret = cls.get_session_secret(db_path=db_path, fallback_db=fallback_db)
sig = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()
return f"{payload}:{sig}"
@classmethod
def verify_session_token(
cls,
token: Optional[str],
max_age_hours: int = 24,
db_path: Optional[str] = None,
fallback_db: str = "logar_state.db"
) -> Optional[str]:
"""Verifies session token authenticity and age."""
if not token or ":" not in token:
return None
parts = token.split(":")
if len(parts) != 4:
return None
username, ts_str, nonce, sig = parts
payload = f"{username}:{ts_str}:{nonce}"
secret = cls.get_session_secret(db_path=db_path, fallback_db=fallback_db)
expected_sig = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected_sig):
return None
try:
ts = int(ts_str)
if datetime.now(timezone.utc).timestamp() - ts > max_age_hours * 3600:
return None
except Exception:
return None
return username
class RateLimiter:
"""Sliding-window IP rate limiter with persistent SQLite tracking and memory fallback."""
def __init__(self):
self._lock = threading.Lock()
self._login_failures: Dict[str, List[float]] = {}
self._enroll_failures: Dict[str, List[float]] = {}
@staticmethod
def get_client_ip(request: Optional[Request] = None) -> str:
"""Extracts client IP address respecting reverse-proxy headers."""
if not request:
return "127.0.0.1"
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
return forwarded.split(",")[0].strip()
real_ip = request.headers.get("x-real-ip")
if real_ip:
return real_ip.strip()
if request.client:
return request.client.host
return "127.0.0.1"
def check_ip_rate_limit(
self,
ip: str,
store: Any = None,
max_attempts: int = 10,
window_seconds: int = 60,
scope: str = "login",
db_path: Optional[str] = None,
fallback_db: str = "logar_state.db"
) -> bool:
"""Checks whether the client IP has exceeded failed attempts within the sliding window."""
now = datetime.now(timezone.utc).timestamp()
cutoff = now - window_seconds
target_db = db_path or fallback_db
try:
conn = sqlite3.connect(target_db, timeout=5.0)
c = conn.cursor()
c.execute("CREATE TABLE IF NOT EXISTS rate_limits (ip TEXT NOT NULL, scope TEXT NOT NULL, attempt_time REAL NOT NULL)")
c.execute("DELETE FROM rate_limits WHERE attempt_time < ?", (cutoff,))
c.execute("SELECT COUNT(*) FROM rate_limits WHERE ip = ? AND scope = ? AND attempt_time >= ?", (ip, scope, cutoff))
cnt = c.fetchone()[0]
conn.commit()
conn.close()
return cnt < max_attempts
except Exception:
with self._lock:
effective_store = store if isinstance(store, dict) else (self._login_failures if scope == "login" else self._enroll_failures)
history = effective_store.get(ip, [])
history = [ts for ts in history if now - ts < window_seconds]
effective_store[ip] = history
return len(history) < max_attempts
def record_failed_ip_attempt(
self,
ip: str,
store: Any = None,
scope: str = "login",
db_path: Optional[str] = None,
fallback_db: str = "logar_state.db"
):
"""Records a persistent failed attempt timestamp for the client IP and scope."""
now = datetime.now(timezone.utc).timestamp()
target_db = db_path or fallback_db
try:
conn = sqlite3.connect(target_db, timeout=5.0)
c = conn.cursor()
c.execute("CREATE TABLE IF NOT EXISTS rate_limits (ip TEXT NOT NULL, scope TEXT NOT NULL, attempt_time REAL NOT NULL)")
c.execute("INSERT INTO rate_limits (ip, scope, attempt_time) VALUES (?, ?, ?)", (ip, scope, now))
conn.commit()
conn.close()
except Exception:
pass
with self._lock:
effective_store = store if isinstance(store, dict) else (self._login_failures if scope == "login" else self._enroll_failures)
if ip not in effective_store:
effective_store[ip] = []
effective_store[ip].append(now)
def reset_failed_ip_attempts(
self,
ip: str,
store: Any = None,
scope: str = "login",
db_path: Optional[str] = None,
fallback_db: str = "logar_state.db"
):
"""Resets persistent failed attempt records for client IP upon successful authentication."""
target_db = db_path or fallback_db
try:
conn = sqlite3.connect(target_db, timeout=5.0)
c = conn.cursor()
c.execute("DELETE FROM rate_limits WHERE ip = ? AND scope = ?", (ip, scope))
conn.commit()
conn.close()
except Exception:
pass
with self._lock:
effective_store = store if isinstance(store, dict) else (self._login_failures if scope == "login" else self._enroll_failures)
if ip in effective_store:
del effective_store[ip]
class PasswordManager:
"""PBKDF2-HMAC-SHA256 password hashing and validation."""
@staticmethod
def hash_password(password: str, salt: Optional[str] = None) -> Tuple[str, str]:
"""Hashes a password using PBKDF2-HMAC-SHA256 with 100,000 iterations."""
if not salt:
salt = secrets.token_hex(16)
pwd_hash = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 100000).hex()
return pwd_hash, salt
@staticmethod
def verify_password(password: str, salt: str, password_hash: str) -> bool:
"""Verifies a password against the stored salt and PBKDF2 hash."""
test_hash, _ = PasswordManager.hash_password(password, salt=salt)
return hmac.compare_digest(test_hash, password_hash)
@staticmethod
def verify_user_credentials(username: str, password: str, db_path: str = "logar_state.db") -> bool:
"""Checks user credentials against SQLite users table."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("SELECT password_hash, salt FROM users WHERE username = ?", (username,))
row = c.fetchone()
conn.close()
if not row:
return False
return PasswordManager.verify_password(password, row[1], row[0])
@staticmethod
def change_user_password(username: str, new_password: str, db_path: str = "logar_state.db") -> bool:
"""Updates a user's password."""
pwd_hash, salt = PasswordManager.hash_password(new_password)
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("UPDATE users SET password_hash = ?, salt = ? WHERE username = ?", (pwd_hash, salt, username))
changed = c.rowcount > 0
conn.commit()
conn.close()
return changed
class TOTPManager:
"""RFC 6238 Two-Factor Authentication (TOTP) generator and validator."""
@staticmethod
def generate_totp_secret(length: int = 20) -> str:
"""Generates a cryptographically random Base32-encoded TOTP secret key."""
return base64.b32encode(secrets.token_bytes(length)).decode("utf-8").replace("=", "")
@staticmethod
def get_totp_token(secret: str, intervals_no: Optional[int] = None) -> str:
"""Calculates a 6-digit TOTP code for the given counter interval according to RFC 6238."""
if intervals_no is None:
intervals_no = int(time.time()) // 30
key = base64.b32decode(secret + "=" * ((8 - len(secret) % 8) % 8), casefold=True)
msg = struct.pack(">Q", intervals_no)
h = hmac.new(key, msg, hashlib.sha1).digest()
o = h[19] & 15
h_int = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
return f"{h_int:06d}"
@staticmethod
def verify_totp_code(secret: str, code: str, window: int = 1) -> bool:
"""Verifies a 6-digit TOTP code with standard drift window (+/- 30s)."""
if not secret or not code:
return False
code = str(code).strip().replace(" ", "")
if len(code) != 6 or not code.isdigit():
return False
now = int(time.time())
current_step = now // 30
for offset in range(-window, window + 1):
if hmac.compare_digest(TOTPManager.get_totp_token(secret, current_step + offset), code):
return True
return False
@staticmethod
def get_totp_uri(username: str, secret: str, issuer: str = "LOGAR Server Hub") -> str:
"""Generates standard otpauth URI for QR code generation."""
user_enc = urllib.parse.quote(username)
issuer_enc = urllib.parse.quote(issuer)
return f"otpauth://totp/{issuer_enc}:{user_enc}?secret={secret}&issuer={issuer_enc}&algorithm=SHA1&digits=6&period=30"
@staticmethod
def get_user_totp_info(username: str, db_path: str = "logar_state.db") -> Dict[str, Any]:
"""Retrieves TOTP configuration for a user from the database."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
try:
c.execute("SELECT totp_secret, totp_enabled FROM users WHERE username = ?", (username,))
row = c.fetchone()
if row:
return {"secret": row[0] or "", "enabled": bool(row[1])}
except Exception:
pass
finally:
conn.close()
return {"secret": "", "enabled": False}
@staticmethod
def enable_user_totp(username: str, secret: str, db_path: str = "logar_state.db") -> bool:
"""Enables and activates TOTP for a user in the database."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
try:
c.execute("UPDATE users SET totp_secret = ?, totp_enabled = 1 WHERE username = ?", (secret, username))
changed = c.rowcount > 0
conn.commit()
return changed
finally:
conn.close()
@staticmethod
def disable_user_totp(username: str, db_path: str = "logar_state.db") -> bool:
"""Disables TOTP for a user in the database."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
try:
c.execute("UPDATE users SET totp_secret = NULL, totp_enabled = 0 WHERE username = ?", (username,))
changed = c.rowcount > 0
conn.commit()
return changed
finally:
conn.close()
class HermesTokenManager:
"""Manages Bearer API tokens for Hermes AI anomaly integration."""
@staticmethod
def create_hermes_token(name: str, token: Optional[str] = None, db_path: str = "logar_state.db") -> Dict[str, Any]:
"""Creates or saves a named Hermes API token."""
if not token:
token = f"hermes_{secrets.token_hex(20)}"
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO hermes_tokens (token, name, created_at, status) VALUES (?, ?, ?, 'active')",
(token, name, now_iso)
)
conn.commit()
conn.close()
return {"token": token, "name": name, "created_at": now_iso, "status": "active"}
@staticmethod
def list_hermes_tokens(db_path: str = "logar_state.db") -> List[Dict[str, Any]]:
"""Lists all registered Hermes access tokens."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("SELECT token, name, created_at, last_used, status FROM hermes_tokens ORDER BY created_at DESC")
rows = [
{"token": r[0], "name": r[1], "created_at": r[2], "last_used": r[3] or "Never", "status": r[4]}
for r in c.fetchall()
]
conn.close()
return rows
@staticmethod
def revoke_hermes_token(token: str, db_path: str = "logar_state.db") -> bool:
"""Revokes an active Hermes token."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("UPDATE hermes_tokens SET status = 'revoked' WHERE token = ?", (token,))
changed = c.rowcount > 0
conn.commit()
conn.close()
return changed
@staticmethod
def delete_hermes_token(token: str, db_path: str = "logar_state.db") -> bool:
"""Deletes a Hermes token."""
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("DELETE FROM hermes_tokens WHERE token = ?", (token,))
changed = c.rowcount > 0
conn.commit()
conn.close()
return changed
@staticmethod
def verify_hermes_token(token: str, db_path: str = "logar_state.db") -> bool:
"""Verifies a Hermes token and updates last_used timestamp."""
if not token:
return False
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("SELECT token FROM hermes_tokens WHERE token = ? AND status = 'active'", (token,))
row = c.fetchone()
if row:
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
c.execute("UPDATE hermes_tokens SET last_used = ? WHERE token = ?", (now_iso, token))
conn.commit()
conn.close()
return row is not None
+114
View File
@@ -0,0 +1,114 @@
"""
LOGAR - Ingestion Queue Manager
Provides a high-throughput thread-safe ring buffer and batch processor for incoming telemetry.
Eliminates disk write contention by decoupling TCP frame ingress from physical database writes.
"""
import time
import threading
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional, Callable
class IngestQueueManager:
"""
High-throughput in-memory buffering and batch processor for incoming telemetry and log anomalies.
Eliminates SQLite / SQL disk write contention by decoupling TCP frame ingress from physical DB writes.
"""
def __init__(self, flush_interval: float = 1.0, batch_size: int = 100, max_queue: int = 50000):
self.flush_interval = flush_interval
self.batch_size = batch_size
self.max_queue = max_queue
self._lock = threading.Lock()
self._buffer: List[Dict[str, Any]] = []
self._total_ingested = 0
self._total_flushed = 0
self._dropped_count = 0
self._last_flush_time = "Never"
self._avg_flush_latency_ms = 0.0
self._flush_count = 0
def push(self, payload: Dict[str, Any]) -> bool:
"""Pushes a log payload into the ring buffer."""
with self._lock:
if len(self._buffer) >= self.max_queue:
self._dropped_count += 1
return False
self._buffer.append(payload)
self._total_ingested += len(payload.get("logs", []))
return True
def get_stats(self) -> Dict[str, Any]:
"""Returns live statistics about queue occupancy, throughput, and flush latency."""
with self._lock:
return {
"queue_size": len(self._buffer),
"total_ingested": self._total_ingested,
"total_flushed": self._total_flushed,
"dropped_count": self._dropped_count,
"last_flush_time": self._last_flush_time,
"avg_flush_latency_ms": round(self._avg_flush_latency_ms, 2),
"flush_count": self._flush_count,
"flush_interval_seconds": self.flush_interval,
"batch_size_threshold": self.batch_size
}
def flush(
self,
db_path: Optional[str] = None,
window_hours: int = 12,
min_runs: int = 1,
process_callback: Optional[Callable[..., Dict[str, Any]]] = None
) -> Dict[str, Any]:
"""Flushes all queued items to persistent storage."""
with self._lock:
if not self._buffer:
return {"status": "noop", "flushed_runs": 0, "processed_logs": 0}
items_to_process = list(self._buffer)
self._buffer.clear()
start_time = time.time()
target_db = db_path or "logar_state.db"
total_processed = 0
promoted_total = 0
try:
for payload in items_to_process:
if process_callback:
res = process_callback(
payload=payload,
db_path=target_db,
window_hours=window_hours,
min_runs=min_runs
)
else:
# Fallback if imported directly
from src import Server
res = Server.process_ingested_logs(
payload=payload,
db_path=target_db,
window_hours=window_hours,
min_runs=min_runs
)
total_processed += res.get("processed", 0)
promoted_total += res.get("promoted_verified", 0)
except Exception as e:
print(f"[!] Error during batch flush to DB: {e}")
elapsed_ms = (time.time() - start_time) * 1000.0
with self._lock:
self._total_flushed += total_processed
self._flush_count += 1
self._avg_flush_latency_ms = (
(self._avg_flush_latency_ms * (self._flush_count - 1) + elapsed_ms) / self._flush_count
)
self._last_flush_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
return {
"status": "success",
"flushed_runs": len(items_to_process),
"processed_logs": total_processed,
"promoted_verified": promoted_total,
"flush_latency_ms": round(elapsed_ms, 2)
}
+225
View File
@@ -0,0 +1,225 @@
"""
LOGAR - Licensing & Sub-CA Introspection Manager
Provides modular classes for PKI X.509 extension parsing, capability gating,
Subordinate CA seat tracking, and asynchronous license reporting.
"""
import os
import json
import socket
import sqlite3
import threading
from typing import Optional, Dict, Any, List, Tuple
try:
from src import server_enrollment as enrollment
except ImportError:
import server_enrollment as enrollment
class LicenseManager:
"""Manages PKI licensing introspection, feature unlocking, and upstream seat syncing."""
DEFAULT_DB_FILE = "logar_state.db"
@staticmethod
def export_license_request_lic(config: Dict[str, Any]) -> Tuple[Dict[str, Any], str]:
"""Generates CA CSR and packages into exportable .lic dictionary and JSON string."""
cert_dir = config.get("cert_dir", "certs")
lic_server = config.get("license_server_url", "https://logar.eibl.tech")
hostname = socket.gethostname()
seats = config.get("max_seats", 10)
fp = config.get("server_fingerprint", "")
return enrollment.create_license_request_lic(
cert_dir=cert_dir,
license_server=lic_server,
server_fingerprint=fp,
hostname=hostname,
requested_seats=seats
)
@staticmethod
def get_active_licensed_features(db_path: Optional[str] = None, server_state: Optional[Dict[str, Any]] = None) -> List[str]:
"""
Returns the list of all active enterprise capability identifiers
authorized either via X.509 certificate extensions or the persistent database license config.
"""
features = set()
state = server_state or {}
# 1. Check in-memory / active Subordinate CA certificate
ca_pem = state.get("ca_cert_pem")
if not ca_pem:
cert_dir = state.get("config", {}).get("cert_dir", "certs")
ca_file = os.path.join(cert_dir, "ca.crt")
if os.path.exists(ca_file):
try:
with open(ca_file, "r", encoding="utf-8") as f:
ca_pem = f.read()
except Exception:
pass
if ca_pem:
try:
info = enrollment.extract_license_info_from_cert(ca_pem)
for feat in info.get("features", []):
if feat:
features.add(str(feat).strip())
except Exception:
pass
# 2. Check license_config table in database
target_db = db_path or state.get("config", {}).get("db_path", LicenseManager.DEFAULT_DB_FILE)
if target_db and os.path.exists(target_db):
try:
conn = sqlite3.connect(target_db, timeout=3.0)
c = conn.cursor()
c.execute("SELECT features FROM license_config WHERE id = 1")
row = c.fetchone()
conn.close()
if row and row[0]:
feat_raw = row[0]
if isinstance(feat_raw, str):
if feat_raw.startswith("["):
feat_list = json.loads(feat_raw)
else:
feat_list = [f.strip() for f in feat_raw.split(",") if f.strip()]
for f_item in feat_list:
if f_item:
features.add(str(f_item).strip())
elif isinstance(feat_raw, list):
for f_item in feat_raw:
if f_item:
features.add(str(f_item).strip())
except Exception:
pass
return sorted(list(features))
@staticmethod
def is_feature_licensed(feature: str, db_path: Optional[str] = None, server_state: Optional[Dict[str, Any]] = None) -> bool:
"""Checks whether a specific enterprise capability (e.g. 'enterprise_db') is authorized."""
if not feature:
return False
return feature in LicenseManager.get_active_licensed_features(db_path=db_path, server_state=server_state)
@staticmethod
def get_license_summary(server_state: Optional[Dict[str, Any]] = None, db_path: Optional[str] = None) -> Dict[str, Any]:
"""Inspects certs/ca.crt, database quotas, and returns comprehensive licensing status."""
state = server_state or {}
config = state.get("config", {})
cert_dir = config.get("cert_dir", "certs")
ca_path = os.path.join(cert_dir, "ca.crt")
target_db = db_path or config.get("db_path", LicenseManager.DEFAULT_DB_FILE)
max_seats = config.get("max_seats", 0)
license_status_db = "unlicensed"
try:
conn = sqlite3.connect(target_db)
c = conn.cursor()
c.execute("SELECT max_seats, license_status FROM license_config WHERE id = 1")
row = c.fetchone()
if row:
if row[0] is not None:
max_seats = row[0]
if len(row) > 1 and row[1]:
license_status_db = row[1]
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'active' AND cert_fingerprint != 'LOCAL-HOST-BUILTIN'")
active_seats = c.fetchone()[0]
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'revoked' AND cert_fingerprint != 'LOCAL-HOST-BUILTIN'")
revoked_seats = c.fetchone()[0]
conn.close()
except Exception:
active_seats = 0
revoked_seats = 0
csr_path = os.path.join(cert_dir, "ca.csr")
csr_exists = os.path.exists(csr_path)
if os.path.exists(ca_path):
try:
with open(ca_path, "r", encoding="utf-8") as f:
ca_pem = f.read()
info = enrollment.extract_license_info_from_cert(ca_pem)
if info.get("seats"):
max_seats = info["seats"]
except Exception:
info = {"valid": False}
else:
info = {"valid": False, "is_self_signed": False, "is_expired": False, "days_remaining": 0}
license_id = info.get("license_id") or "UNLICENSED"
issuer_url = info.get("issuer_url") or config.get("license_server_url", "https://logar.eibl.tech")
if info.get("is_expired"):
status_label = "EXPIRED"
elif info.get("seats") and not info.get("is_self_signed"):
status_label = "ACTIVE"
elif max_seats > 0 and license_status_db == "active":
status_label = "ACTIVE"
elif info.get("is_self_signed"):
status_label = "SELF-SIGNED" if max_seats > 0 else "UNLICENSED"
else:
status_label = "UNLICENSED" if max_seats <= 0 else "ACTIVE"
pct = int((active_seats / max_seats * 100)) if max_seats > 0 else 0
return {
"status": status_label,
"active_seats": active_seats,
"active_seats_taken": active_seats,
"revoked_seats": revoked_seats,
"max_seats": max_seats,
"available_seats": max(0, max_seats - active_seats),
"percentage": min(100, pct),
"license_id": license_id,
"issuer_url": issuer_url,
"issuer_cn": info.get("issuer_cn", "N/A"),
"subject_cn": info.get("subject_cn", "N/A"),
"is_self_signed": info.get("is_self_signed", False),
"not_valid_before": info.get("not_valid_before", "N/A"),
"not_valid_after": info.get("not_valid_after", "N/A"),
"days_remaining": info.get("days_remaining", 0),
"fingerprint": info.get("fingerprint", "N/A"),
"csr_exists": csr_exists,
"server_fingerprint": config.get("server_fingerprint", "N/A"),
"features": LicenseManager.get_active_licensed_features(db_path=target_db, server_state=state)
}
@staticmethod
def report_seat_quota_to_authority_sync(server_state: Optional[Dict[str, Any]] = None):
"""Background helper to push active seat count to upstream authority without blocking."""
try:
state = server_state or {}
config = state.get("config", {})
cert_dir = config.get("cert_dir", "certs")
db_path = config.get("db_path", LicenseManager.DEFAULT_DB_FILE)
lic_server = config.get("license_server_url", "https://logar.eibl.tech")
if not os.path.exists(os.path.join(cert_dir, "ca.crt")) or not os.path.exists(os.path.join(cert_dir, "ca.key")):
return
active_seats = 0
if os.path.exists(db_path):
try:
conn = sqlite3.connect(db_path)
row = conn.execute("SELECT COUNT(*) FROM clients WHERE status = 'active' AND cert_fingerprint != 'LOCAL-HOST-BUILTIN'").fetchone()
if row:
active_seats = row[0]
conn.close()
except Exception:
pass
enrollment.renew_ca_cert_mtls(
cert_dir=cert_dir,
license_server_url=lic_server,
seats_taken=active_seats,
active_seats=active_seats,
hostname=config.get("server_name", ""),
server_fingerprint=config.get("server_fingerprint", "")
)
except Exception:
pass
@staticmethod
def trigger_async_seat_report(server_state: Optional[Dict[str, Any]] = None):
"""Fires a non-blocking background thread to sync seat usage with upstream authority."""
threading.Thread(target=LicenseManager.report_seat_quota_to_authority_sync, args=(server_state,), daemon=True).start()
+141
View File
@@ -0,0 +1,141 @@
"""
LOGAR - Dynamic PKI & mTLS Context Manager
Provides modular classes for TLS 1.3 socket contexts, in-flight certificate rotation,
and automated Sub-CA renewal.
"""
import os
import ssl
import sqlite3
from typing import Optional, List, Dict, Any
try:
from src import server_enrollment as enrollment
except ImportError:
import server_enrollment as enrollment
class PKIManager:
"""Manages server TLS certificates, mTLS SSL contexts, and hot certificate reloading."""
@staticmethod
def init_mtls_server_context(cert_dir: str = "certs") -> ssl.SSLContext:
"""Initializes TLS 1.3 server SSLContext with client certificate requirement (mTLS)."""
ca_file = os.path.join(cert_dir, "ca.crt")
srv_cert = os.path.join(cert_dir, "server.crt")
srv_key = os.path.join(cert_dir, "server.key")
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
ctx.load_verify_locations(cafile=ca_file)
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
if os.path.exists(upstream_file):
try:
ctx.load_verify_locations(cafile=upstream_file)
except Exception:
pass
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
return ctx
@staticmethod
def reload_mtls_context(ssl_ctx: ssl.SSLContext, cert_dir: str = "certs"):
"""
Dynamically reloads server certificate chain and Root CA in an active SSLContext.
Allows in-flight TLS certificate rotation without dropping the listening socket.
"""
ca_file = os.path.join(cert_dir, "ca.crt")
srv_cert = os.path.join(cert_dir, "server.crt")
srv_key = os.path.join(cert_dir, "server.key")
ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
ssl_ctx.load_verify_locations(cafile=ca_file)
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
if os.path.exists(upstream_file):
try:
ssl_ctx.load_verify_locations(cafile=upstream_file)
except Exception:
pass
@staticmethod
def check_and_rotate_server_certs(
cert_dir: str = "certs",
hostnames: Optional[List[str]] = None,
threshold_days: int = 30,
server_state: Optional[Dict[str, Any]] = None
) -> bool:
"""
Checks if Root/Subordinate CA or server TLS certificate are expiring within threshold_days.
If a licensed Subordinate CA is expiring, automatically connects over mTLS to logar.eibl.tech
to request an in-flight renewal, then reloads the active SSLContext without socket interruption.
Returns True if renewed/reloaded, False otherwise.
"""
state = server_state or {}
config = state.get("config", {})
license_server = config.get("license_server_url", "https://logar.eibl.tech")
ca_cert_path = os.path.join(cert_dir, "ca.crt")
# 1. Check if CA certificate is expiring and attempt upstream mTLS renewal
if os.path.exists(ca_cert_path):
try:
with open(ca_cert_path, "r", encoding="utf-8") as f:
ca_pem = f.read()
if enrollment.is_cert_expiring_soon(ca_pem, threshold_days=threshold_days):
info = enrollment.extract_license_info_from_cert(ca_pem)
if not info.get("is_self_signed", True) or info.get("license_id"):
print("[*] Subordinate CA expiring soon. Initiating automated mTLS renewal with logar.eibl.tech...")
success, msg, new_pem = enrollment.renew_ca_cert_mtls(
cert_dir=cert_dir,
license_server_url=license_server
)
if success and new_pem:
print(f"[+] Automated mTLS renewal successful: {msg}")
new_info = enrollment.extract_license_info_from_cert(new_pem)
if new_info.get("seats"):
config["max_seats"] = new_info["seats"]
db_path = config.get("db_path", "logar_state.db")
conn = sqlite3.connect(db_path)
conn.execute("UPDATE license_config SET max_seats = ? WHERE id = 1", (new_info["seats"],))
conn.commit()
conn.close()
ca_cert, ca_key, ca_pem, ca_key_pem = enrollment.generate_ca_if_needed(cert_dir=cert_dir, force_renew=False)
state["ca_cert"] = ca_cert
state["ca_key"] = ca_key
state["ca_cert_pem"] = ca_pem
enrollment.generate_server_cert_if_needed(ca_cert, ca_key, hostnames=hostnames, cert_dir=cert_dir, force_renew=True)
ssl_ctx = state.get("ssl_ctx")
if ssl_ctx is not None:
try:
PKIManager.reload_mtls_context(ssl_ctx, cert_dir=cert_dir)
print("[+] In-flight mTLS SSLContext successfully reloaded with renewed CA.")
except Exception as e:
print(f"[!] Failed to reload in-flight SSLContext: {e}")
return True
else:
print(f"[!] Automated mTLS renewal attempt failed: {msg}")
except Exception as e:
print(f"[!] Error evaluating CA certificate for mTLS renewal: {e}")
# 2. Local self-healing / regeneration check
ca_renewed, srv_renewed = enrollment.check_and_renew_hub_pki(
cert_dir=cert_dir,
hostnames=hostnames,
threshold_days=threshold_days
)
if ca_renewed or srv_renewed:
print(f"[!] Server Hub PKI certificates renewed (CA renewed: {ca_renewed}, Server cert renewed: {srv_renewed}).")
ca_cert, ca_key, ca_pem, ca_key_pem = enrollment.generate_ca_if_needed(cert_dir=cert_dir, force_renew=False)
state["ca_cert"] = ca_cert
state["ca_key"] = ca_key
state["ca_cert_pem"] = ca_pem
ssl_ctx = state.get("ssl_ctx")
if ssl_ctx is not None:
try:
PKIManager.reload_mtls_context(ssl_ctx, cert_dir=cert_dir)
print("[+] In-flight mTLS SSLContext successfully reloaded with updated certificates.")
except Exception as e:
print(f"[!] Failed to reload in-flight SSLContext: {e}")
return True
return False
+76
View File
@@ -0,0 +1,76 @@
"""
LOGAR - Server Pydantic Models
Provides structured request and response models for the LOGAR Server Hub API.
"""
from typing import Optional
from pydantic import BaseModel
class ClientEnrollRequest(BaseModel):
client_id: str
hostname: str
os: str
enrollment_secret: str
class ClientActionRequest(BaseModel):
client_id: str
class LicenseImportRequest(BaseModel):
ca_cert: str
upstream_ca: Optional[str] = None
class HermesTokenCreateRequest(BaseModel):
name: str
class HermesTokenRevokeRequest(BaseModel):
token: str
class ClientProfileExportRequest(BaseModel):
server_host: Optional[str] = None
server_port: Optional[int] = None
client_id: Optional[str] = None
class ChangePasswordRequest(BaseModel):
old_password: str
new_password: str
class DbConnectionTestRequest(BaseModel):
host: str = "localhost"
port: int = 5432
database: str = "logar"
user: str = "postgres"
password: str = ""
sslmode: str = "prefer"
connect_timeout: int = 5
pool_size: int = 10
class DbSwitchBackendRequest(BaseModel):
db_type: str = "sqlite" # "sqlite" | "postgres"
host: Optional[str] = "localhost"
port: Optional[int] = 5432
database: Optional[str] = "logar"
user: Optional[str] = "postgres"
password: Optional[str] = ""
sslmode: Optional[str] = "prefer"
connect_timeout: Optional[int] = 5
pool_size: Optional[int] = 10
db_path: Optional[str] = None
class DbFullConfigRequest(BaseModel):
storage_engine: str = "sqlite" # "sqlite" | "postgres"
cache_mode: str = "direct" # "direct" | "buffered"
flush_interval: float = 2.0
batch_size: int = 100
max_queue: int = 50000
encrypt_at_rest: bool = False
pg_config: Optional[DbConnectionTestRequest] = None
+638
View File
@@ -0,0 +1,638 @@
"""
LOGAR - Multi-Backend Storage Engine & Data-at-Rest Crypto
Provides modular classes for Database schema management, SQLite WAL persistence,
PostgreSQL enterprise backend, and OpenPGP / AES-256-GCM data encryption at rest.
"""
import os
import sys
import json
import sqlite3
import secrets
from datetime import datetime, timezone
from typing import Optional, Dict, Any, List, Tuple, Callable
import pgpy
from pgpy.constants import (
PubKeyAlgorithm,
KeyFlags,
HashAlgorithm,
SymmetricKeyAlgorithm,
CompressionAlgorithm
)
from src.auth_manager import PasswordManager
class StorageCryptoManager:
"""Manages OpenPGP keypairs and AES-256-GCM zero-knowledge data-at-rest encryption."""
@staticmethod
def generate_server_keypair(server_name: str) -> Tuple[str, str, str]:
"""Generates an OpenPGP RSA 2048 key with encryption capability."""
key = pgpy.PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 2048)
uid = pgpy.PGPUID.new(server_name)
key.add_uid(
uid,
usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
hashes=[HashAlgorithm.SHA256, HashAlgorithm.SHA512],
ciphers=[SymmetricKeyAlgorithm.AES256, SymmetricKeyAlgorithm.AES128],
compression=[
CompressionAlgorithm.ZIP,
CompressionAlgorithm.ZLIB,
CompressionAlgorithm.BZ2,
CompressionAlgorithm.Uncompressed
]
)
private_key_armored = str(key)
public_key_armored = str(key.pubkey)
fingerprint = str(key.pubkey.fingerprint)
return private_key_armored, public_key_armored, fingerprint
@staticmethod
def get_server_public_key(server_state: Optional[Dict[str, Any]] = None) -> Optional[pgpy.PGPKey]:
"""Retrieves or parses the server's OpenPGP public key for encryption operations."""
state = server_state or {}
pub_obj = state.get("public_key_obj")
if pub_obj:
return pub_obj
priv_obj = state.get("private_key_obj")
if priv_obj and hasattr(priv_obj, "pubkey"):
state["public_key_obj"] = priv_obj.pubkey
return priv_obj.pubkey
pub_blob = state.get("config", {}).get("public_key")
if pub_blob:
try:
key, _ = pgpy.PGPKey.from_blob(pub_blob)
state["public_key_obj"] = key
return key
except Exception:
pass
return None
@staticmethod
def encrypt_field_for_storage(text: str, force: bool = False, server_state: Optional[Dict[str, Any]] = None, is_unlocked: bool = False) -> str:
"""Encrypts a text payload using the server's OpenPGP public key if encrypt_at_rest is enabled."""
if not text or not isinstance(text, str):
return text
state = server_state or {}
config = state.get("config", {})
should_encrypt = force or (is_unlocked and bool(config.get("encrypt_at_rest", False)))
if not should_encrypt:
return text
if text.startswith("-----BEGIN PGP MESSAGE-----"):
return text
pub_key = StorageCryptoManager.get_server_public_key(server_state=state)
if not pub_key:
return text
try:
msg = pgpy.PGPMessage.new(text)
encrypted = pub_key.encrypt(msg)
return str(encrypted)
except Exception as e:
print(f"[!] Warning: Failed to encrypt field at rest: {e}")
return text
@staticmethod
def decrypt_field_from_storage(text: str, server_state: Optional[Dict[str, Any]] = None) -> str:
"""
Decrypts an OpenPGP armored message string using the server's private key.
Returns cleartext for authorized display and API consumption.
"""
if not text or not isinstance(text, str):
return text
if not text.startswith("-----BEGIN PGP MESSAGE-----"):
return text
state = server_state or {}
priv_key = state.get("private_key_obj")
if not priv_key:
priv_blob = state.get("config", {}).get("private_key")
if priv_blob:
try:
priv_key, _ = pgpy.PGPKey.from_blob(priv_blob)
state["private_key_obj"] = priv_key
except Exception:
pass
if not priv_key:
return text
try:
pgp_msg = pgpy.PGPMessage.from_blob(text)
decrypted = priv_key.decrypt(pgp_msg)
return str(decrypted.message)
except Exception as e:
print(f"[!] Warning: Failed to decrypt field from storage: {e}")
return text
class StorageEngine:
"""Manages database schema initialization, migrations, multi-engine switching, and telemetry stats."""
DEFAULT_DB_FILE = "logar_state.db"
DEFAULT_ADMIN_USER = os.environ.get("LOGAR_ADMIN_USER", "admin")
DEFAULT_ADMIN_PASS = os.environ.get("LOGAR_ADMIN_PASS", "admin_logar_2026!")
@classmethod
def init_db(cls, db_path: str, enrollment_secret: Optional[str] = None, max_seats: int = 0) -> str:
"""Initializes the SQLite schema for multi-run temporal tracking, client tracking, users, and license quota."""
def _execute_schema(target_path: str):
target_dir = os.path.dirname(os.path.abspath(target_path))
if target_dir:
os.makedirs(target_dir, exist_ok=True)
conn = sqlite3.connect(target_path, timeout=10.0)
try:
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA busy_timeout=5000;")
except Exception:
pass
# 1. Active Issues
conn.execute("""
CREATE TABLE IF NOT EXISTS active_issues (
fingerprint TEXT PRIMARY KEY,
site_name TEXT,
server TEXT,
signature TEXT,
severity TEXT,
message TEXT,
os_type TEXT,
first_seen TEXT,
last_seen TEXT,
run_count INTEGER,
status TEXT,
last_run_id TEXT
)
""")
# 2. Ingest Runs
conn.execute("""
CREATE TABLE IF NOT EXISTS ingest_runs (
run_id TEXT PRIMARY KEY,
site_name TEXT,
server TEXT,
timestamp TEXT,
log_count INTEGER
)
""")
# 3. License Config
conn.execute("""
CREATE TABLE IF NOT EXISTS license_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
max_seats INTEGER NOT NULL DEFAULT 0,
enrollment_secret TEXT NOT NULL,
license_id TEXT,
license_server_url TEXT,
issued_at TEXT,
expires_at TEXT,
license_status TEXT DEFAULT 'unlicensed'
)
""")
# Migrations for license_config
for col, col_def in [
("license_id", "TEXT"),
("license_server_url", "TEXT"),
("issued_at", "TEXT"),
("expires_at", "TEXT"),
("license_status", "TEXT DEFAULT 'unlicensed'"),
("features", "TEXT DEFAULT '[]'")
]:
try:
conn.execute(f"ALTER TABLE license_config ADD COLUMN {col} {col_def}")
except sqlite3.OperationalError:
pass
# 4. Clients Table
conn.execute("""
CREATE TABLE IF NOT EXISTS clients (
client_id TEXT PRIMARY KEY,
hostname TEXT NOT NULL,
os_type TEXT NOT NULL,
cert_fingerprint TEXT NOT NULL,
status TEXT DEFAULT 'active',
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Migrations for clients table
for col, col_def in [
("metrics_json", "TEXT DEFAULT '{}'"),
("cpu_percent", "REAL DEFAULT 0.0"),
("memory_percent", "REAL DEFAULT 0.0"),
("disk_percent_free", "REAL DEFAULT 0.0")
]:
try:
conn.execute(f"ALTER TABLE clients ADD COLUMN {col} {col_def}")
except sqlite3.OperationalError:
pass
# 5. Users Table
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'admin',
created_at TEXT NOT NULL,
totp_secret TEXT,
totp_enabled INTEGER DEFAULT 0
)
""")
# Migrations for users table (2FA)
for col, col_def in [
("totp_secret", "TEXT"),
("totp_enabled", "INTEGER DEFAULT 0")
]:
try:
conn.execute(f"ALTER TABLE users ADD COLUMN {col} {col_def}")
except sqlite3.OperationalError:
pass
# Ensure default admin user exists
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM users")
if c.fetchone()[0] == 0:
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
pwd_hash, salt = PasswordManager.hash_password(cls.DEFAULT_ADMIN_PASS)
conn.execute(
"INSERT INTO users (username, password_hash, salt, role, created_at) VALUES (?, ?, ?, 'admin', ?)",
(cls.DEFAULT_ADMIN_USER, pwd_hash, salt, now_iso)
)
# 6. Hermes Tokens Table
conn.execute("""
CREATE TABLE IF NOT EXISTS hermes_tokens (
token TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
last_used TEXT,
status TEXT NOT NULL DEFAULT 'active'
)
""")
# Auto-initialize initial Hermes token if table is empty
c.execute("SELECT COUNT(*) FROM hermes_tokens")
if c.fetchone()[0] == 0:
initial_token = f"hermes_live_{secrets.token_hex(20)}"
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
conn.execute(
"INSERT INTO hermes_tokens (token, name, created_at, status) VALUES (?, 'Default Hermes Agent', ?, 'active')",
(initial_token, now_iso)
)
# 7. Rate Limits & Server Meta Tables
conn.execute("""
CREATE TABLE IF NOT EXISTS rate_limits (
ip TEXT NOT NULL,
scope TEXT NOT NULL,
attempt_time REAL NOT NULL
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_rate_limits_ip_scope ON rate_limits (ip, scope, attempt_time)")
conn.execute("""
CREATE TABLE IF NOT EXISTS server_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""")
status = "active" if max_seats > 0 else "unlicensed"
conn.execute("""
INSERT OR IGNORE INTO license_config (id, max_seats, enrollment_secret, license_status, features)
VALUES (1, ?, ?, ?, '[]')
""", (max_seats, enrollment_secret or "", status))
conn.commit()
conn.close()
try:
_execute_schema(db_path)
return db_path
except (sqlite3.OperationalError, PermissionError, OSError) as e:
if "readonly" in str(e).lower() or "permission" in str(e).lower() or "locked" in str(e).lower() or isinstance(e, PermissionError):
local_app = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
fallback_dir = os.path.join(local_app, "LOGAR-Server")
os.makedirs(fallback_dir, exist_ok=True)
fallback_path = os.path.join(fallback_dir, "logar_state.db")
if fallback_path != db_path:
print(f"[!] Warning: Database path '{db_path}' is not writable ({e}). Falling back to '{fallback_path}'...")
_execute_schema(fallback_path)
return fallback_path
raise
@staticmethod
def init_postgres_db(pg_config: Dict[str, Any]) -> Tuple[bool, str]:
"""Initializes table schema on a PostgreSQL database instance."""
try:
try:
import psycopg2
except ImportError:
try:
import psycopg as psycopg2
except ImportError:
return False, "PostgreSQL driver not installed on server (install psycopg2-binary or psycopg)"
host = pg_config.get("host", "localhost")
port = int(pg_config.get("port", 5432))
database = pg_config.get("database", "logar")
user = pg_config.get("user", "postgres")
password = pg_config.get("password", "")
sslmode = pg_config.get("sslmode", "prefer")
timeout = int(pg_config.get("connect_timeout", 5))
conn = psycopg2.connect(
host=host, port=port, dbname=database, user=user, password=password, sslmode=sslmode, connect_timeout=timeout
)
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS active_issues (
fingerprint VARCHAR(255) PRIMARY KEY,
site_name VARCHAR(255),
server VARCHAR(255),
signature TEXT,
severity VARCHAR(50),
message TEXT,
os_type VARCHAR(50),
first_seen VARCHAR(50),
last_seen VARCHAR(50),
run_count INTEGER,
status VARCHAR(50),
last_run_id VARCHAR(100)
);
CREATE TABLE IF NOT EXISTS ingest_runs (
run_id VARCHAR(100) PRIMARY KEY,
site_name VARCHAR(255),
server VARCHAR(255),
timestamp VARCHAR(50),
log_count INTEGER
);
CREATE TABLE IF NOT EXISTS license_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
max_seats INTEGER NOT NULL DEFAULT 0,
enrollment_secret TEXT NOT NULL,
license_id VARCHAR(100),
license_server_url VARCHAR(255),
issued_at VARCHAR(50),
expires_at VARCHAR(50),
license_status VARCHAR(50) DEFAULT 'unlicensed',
features TEXT DEFAULT '[]'
);
CREATE TABLE IF NOT EXISTS clients (
client_id VARCHAR(100) PRIMARY KEY,
hostname VARCHAR(255) NOT NULL,
os_type VARCHAR(50) NOT NULL,
cert_fingerprint VARCHAR(255) NOT NULL,
status VARCHAR(50) DEFAULT 'active',
first_seen VARCHAR(50),
last_seen VARCHAR(50)
);
CREATE TABLE IF NOT EXISTS users (
username VARCHAR(100) PRIMARY KEY,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'admin',
created_at VARCHAR(50) NOT NULL,
totp_secret TEXT,
totp_enabled INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS hermes_tokens (
token VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at VARCHAR(50) NOT NULL,
last_used VARCHAR(50),
status VARCHAR(50) NOT NULL DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS rate_limits (
ip VARCHAR(100) NOT NULL,
scope VARCHAR(50) NOT NULL,
attempt_time DOUBLE PRECISION NOT NULL
);
CREATE TABLE IF NOT EXISTS server_meta (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL
);
""")
# Migrations for Postgres clients table
for col, col_def in [
("metrics_json", "TEXT DEFAULT '{}'"),
("cpu_percent", "DOUBLE PRECISION DEFAULT 0.0"),
("memory_percent", "DOUBLE PRECISION DEFAULT 0.0"),
("disk_percent_free", "DOUBLE PRECISION DEFAULT 0.0")
]:
try:
cur.execute(f"ALTER TABLE clients ADD COLUMN IF NOT EXISTS {col} {col_def}")
except Exception:
pass
conn.commit()
conn.close()
return True, "PostgreSQL schema successfully initialized"
except Exception as e:
return False, f"PostgreSQL connection/init failed: {e}"
@staticmethod
def migrate_sqlite_to_postgres(
sqlite_path: str,
pg_config: Dict[str, Any],
encrypt_fn: Optional[Callable[[str], str]] = None
) -> Tuple[bool, str, Dict[str, int]]:
"""Transfers all tables and records from SQLite to PostgreSQL with data validation and encryption."""
if not os.path.exists(sqlite_path):
return False, f"SQLite database not found at {sqlite_path}", {}
ok, init_msg = StorageEngine.init_postgres_db(pg_config)
if not ok:
return False, init_msg, {}
try:
try:
import psycopg2
except ImportError:
import psycopg as psycopg2
host = pg_config.get("host", "localhost")
port = int(pg_config.get("port", 5432))
database = pg_config.get("database", "logar")
user = pg_config.get("user", "postgres")
password = pg_config.get("password", "")
sslmode = pg_config.get("sslmode", "prefer")
timeout = int(pg_config.get("connect_timeout", 5))
pg_conn = psycopg2.connect(
host=host, port=port, dbname=database, user=user, password=password, sslmode=sslmode, connect_timeout=timeout
)
sqlite_conn = sqlite3.connect(sqlite_path)
sqlite_conn.row_factory = sqlite3.Row
migrated_counts = {}
tables = ["active_issues", "ingest_runs", "license_config", "clients", "users", "hermes_tokens", "rate_limits", "server_meta"]
with pg_conn.cursor() as pg_cur:
for tbl in tables:
try:
s_cur = sqlite_conn.cursor()
s_cur.execute(f"SELECT * FROM {tbl}")
rows = s_cur.fetchall()
if rows:
col_names = [col[0] for col in s_cur.description]
cols_joined = ", ".join(col_names)
placeholders = ", ".join(["%s"] * len(col_names))
insert_query = f"INSERT INTO {tbl} ({cols_joined}) VALUES ({placeholders}) ON CONFLICT DO NOTHING;"
data = []
for r in rows:
row_data = [r[c] for c in col_names]
if tbl == "active_issues" and "message" in col_names and encrypt_fn:
msg_idx = col_names.index("message")
row_data[msg_idx] = encrypt_fn(row_data[msg_idx])
data.append(row_data)
pg_cur.executemany(insert_query, data)
migrated_counts[tbl] = len(rows)
else:
migrated_counts[tbl] = 0
except Exception as tbl_err:
print(f"[!] Warning migrating table {tbl}: {tbl_err}")
migrated_counts[tbl] = 0
pg_conn.commit()
sqlite_conn.close()
pg_conn.close()
return True, "Migration completed successfully", migrated_counts
except Exception as e:
return False, f"Migration failed: {e}", {}
@staticmethod
def get_storage_engine_info(
db_path: Optional[str] = None,
server_state: Optional[Dict[str, Any]] = None,
ingest_queue: Optional[Any] = None,
is_unlocked: bool = False
) -> Dict[str, Any]:
"""Returns metadata about the active storage backend, configuration, and database telemetry."""
state = server_state or {}
config = state.get("config", {})
db_type = config.get("db_type", config.get("storage_engine", "sqlite")).lower()
target_db = db_path or config.get("db_path", StorageEngine.DEFAULT_DB_FILE)
table_stats = {"active_issues": 0, "ingest_runs": 0, "clients": 0, "users": 0}
licensed_features = []
if target_db and os.path.exists(target_db):
try:
conn_l = sqlite3.connect(target_db, timeout=3.0)
row_l = conn_l.execute("SELECT features FROM license_config WHERE id = 1").fetchone()
conn_l.close()
if row_l and row_l[0]:
feat_raw = row_l[0]
if isinstance(feat_raw, str):
if feat_raw.startswith("["):
licensed_features = json.loads(feat_raw)
else:
licensed_features = [f.strip() for f in feat_raw.split(",") if f.strip()]
elif isinstance(feat_raw, list):
licensed_features = list(feat_raw)
except Exception:
pass
flush_interval = ingest_queue.flush_interval if ingest_queue else 2.0
batch_size = ingest_queue.batch_size if ingest_queue else 100
max_queue = ingest_queue.max_queue if ingest_queue else 50000
if db_type in ("postgres", "postgresql"):
pg_cfg = config.get("postgres", {})
host = pg_cfg.get("host", "localhost")
port = int(pg_cfg.get("port", 5432))
dbname = pg_cfg.get("database", "logar")
user = pg_cfg.get("user", "postgres")
try:
try:
import psycopg2
except ImportError:
import psycopg as psycopg2
p_conn = psycopg2.connect(
host=host, port=port, dbname=dbname, user=user,
password=pg_cfg.get("password", ""),
sslmode=pg_cfg.get("sslmode", "prefer"),
connect_timeout=int(pg_cfg.get("connect_timeout", 3))
)
with p_conn.cursor() as p_cur:
for tbl in ["active_issues", "ingest_runs", "clients", "users"]:
try:
p_cur.execute(f"SELECT COUNT(*) FROM {tbl}")
r = p_cur.fetchone()
table_stats[tbl] = r[0] if r else 0
except Exception:
pass
p_conn.close()
except Exception:
pass
return {
"engine": "postgres",
"engine_type": "postgres",
"backend_display": "PostgreSQL Enterprise Pool",
"connection_target": f"{user}@{host}:{port}/{dbname}",
"status": "Enterprise Connected",
"table_stats": table_stats,
"is_enterprise": True,
"feature_unlocked": is_unlocked,
"licensed_features": licensed_features,
"cache_mode": config.get("cache_mode", "direct"),
"encrypt_at_rest": config.get("encrypt_at_rest", False),
"flush_interval": flush_interval,
"batch_size": batch_size,
"max_queue": max_queue,
"postgres_config": pg_cfg
}
else:
size_bytes = 0
if target_db and os.path.exists(target_db):
try:
size_bytes = os.path.getsize(target_db)
conn = sqlite3.connect(target_db, timeout=3.0)
for tbl in ["active_issues", "ingest_runs", "clients", "users"]:
try:
row = conn.execute(f"SELECT COUNT(*) FROM {tbl}").fetchone()
table_stats[tbl] = row[0] if row else 0
except Exception:
pass
conn.close()
except Exception:
pass
size_kb = round(size_bytes / 1024, 2)
return {
"engine": "sqlite",
"engine_type": "sqlite",
"backend_display": "SQLite 3 (WAL Mode)",
"connection_target": target_db,
"db_size_kb": size_kb,
"status": "Local WAL Optimized",
"table_stats": table_stats,
"is_enterprise": False,
"feature_unlocked": is_unlocked,
"licensed_features": licensed_features,
"cache_mode": config.get("cache_mode", "direct"),
"encrypt_at_rest": config.get("encrypt_at_rest", False),
"flush_interval": flush_interval,
"batch_size": batch_size,
"max_queue": max_queue,
"postgres_config": config.get("postgres", {
"host": "localhost",
"port": 5432,
"database": "logar",
"user": "postgres",
"password": "",
"sslmode": "prefer",
"connect_timeout": 5,
"pool_size": 10
})
}
+1680
View File
File diff suppressed because it is too large Load Diff