feat(licensing): report active seat quota and usage to upstream license authority
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s

This commit is contained in:
2026-09-25 22:53:26 +02:00
parent c2fed0379c
commit 4396052ed3
3 changed files with 84 additions and 6 deletions
+72 -3
View File
@@ -11,6 +11,7 @@ import secrets
import hashlib
import hmac
import warnings
import threading
from datetime import datetime, timezone, timedelta
from typing import Dict, Any, List, Optional, Tuple
@@ -811,8 +812,46 @@ def check_and_rotate_server_certs(
return False
def report_seat_quota_to_authority_sync():
"""Background helper to push active seat count to upstream authority without blocking."""
try:
config = SERVER_STATE.get("config", {})
cert_dir = config.get("cert_dir", "certs")
db_path = config.get("db_path", 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'").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
def trigger_async_seat_report():
"""Fires a non-blocking background thread to sync seat usage with upstream authority."""
threading.Thread(target=report_seat_quota_to_authority_sync, daemon=True).start()
async def cert_validity_watchdog(interval_seconds: int = 43200, threshold_days: int = 30):
"""Periodically checks the validity of Hub Root CA and Server TLS certificates (default every 12 hours)."""
"""Periodically checks the validity of Hub Root CA and Server TLS certificates and reports seat quotas."""
config = SERVER_STATE.get("config", {})
cert_dir = config.get("cert_dir", "certs")
hostnames = [config.get("tcp_host", "0.0.0.0"), "127.0.0.1", "localhost"]
@@ -820,6 +859,7 @@ async def cert_validity_watchdog(interval_seconds: int = 43200, threshold_days:
try:
await asyncio.sleep(interval_seconds)
check_and_rotate_server_certs(cert_dir=cert_dir, hostnames=hostnames, threshold_days=threshold_days)
report_seat_quota_to_authority_sync()
except asyncio.CancelledError:
break
except Exception as e:
@@ -1132,6 +1172,7 @@ def enroll_client(req: ClientEnrollRequest):
conn.commit()
conn.close()
print(f"[+] Successfully enrolled new client: {req.client_id} ({req.hostname}) [Seats: {active_count + 1}/{max_seats}]")
trigger_async_seat_report()
return {
"ca_cert": ca_cert_pem,
@@ -1231,6 +1272,7 @@ def approve_client(action: ClientActionRequest):
if affected == 0:
raise HTTPException(status_code=404, detail="Client ID not found")
print(f"[+] Approved/Reactivated client seat: {action.client_id}")
trigger_async_seat_report()
return {"status": "active", "client_id": action.client_id}
@@ -1252,6 +1294,7 @@ def revoke_client(action: ClientActionRequest):
if affected == 0:
raise HTTPException(status_code=404, detail="Client ID not found")
print(f"[!] Revoked client license seat: {action.client_id}")
trigger_async_seat_report()
return {"status": "revoked", "client_id": action.client_id}
@@ -1269,6 +1312,7 @@ def delete_client(action: ClientActionRequest):
if affected == 0:
raise HTTPException(status_code=404, detail="Client ID not found")
print(f"[+] Removed client forwarder record: {action.client_id}")
trigger_async_seat_report()
return {"status": "deleted", "client_id": action.client_id}
@@ -1569,11 +1613,29 @@ async def trigger_license_renewal(request: Request):
config = SERVER_STATE.get("config", {})
cert_dir = config.get("cert_dir", "certs")
lic_server = config.get("license_server_url", "https://logar.eibl.tech")
db_path = config.get("db_path", DEFAULT_DB_FILE)
active_seats = 0
try:
conn_cnt = sqlite3.connect(db_path)
c_cnt = conn_cnt.cursor()
c_cnt.execute("SELECT COUNT(*) FROM clients WHERE status = 'active'")
row = c_cnt.fetchone()
if row:
active_seats = row[0]
conn_cnt.close()
except Exception:
pass
success, msg, new_pem = enrollment.renew_ca_cert_mtls(
cert_dir=cert_dir,
license_server_url=lic_server,
requested_seats=requested_seats,
seat_increment=seat_increment
seat_increment=seat_increment,
seats_taken=active_seats,
active_seats=active_seats,
hostname=config.get("server_name", ""),
server_fingerprint=config.get("server_fingerprint", "")
)
if not success or not new_pem:
raise HTTPException(status_code=400, detail=msg)
@@ -2509,7 +2571,14 @@ def main():
# Fallback to mTLS HTTP sync
print(f"[!] WebSocket sync returned: {msg}. Falling back to mTLS renewal...")
http_url = lic_server.replace("wss://", "https://").replace("ws://", "http://").replace("/ws/license", "")
success, msg, new_pem = enrollment.renew_ca_cert_mtls(cert_dir=cert_dir, license_server_url=http_url)
success, msg, new_pem = enrollment.renew_ca_cert_mtls(
cert_dir=cert_dir,
license_server_url=http_url,
seats_taken=active_seats,
active_seats=active_seats,
hostname=config.get("server_name", ""),
server_fingerprint=config.get("server_fingerprint", "")
)
if not success or not new_pem:
print(f"[-] License sync failed: {msg}")
sys.exit(1)