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)
+11 -2
View File
@@ -900,13 +900,17 @@ def renew_ca_cert_mtls(
upstream_ca_pem: Optional[str] = None,
requested_seats: Optional[int] = None,
seat_increment: int = 0,
seats_taken: Optional[int] = None,
active_seats: Optional[int] = None,
hostname: Optional[str] = None,
server_fingerprint: Optional[str] = None,
timeout: int = 15,
verify_tls: bool = True
) -> Tuple[bool, str, Optional[str]]:
"""
Connects to the master License Authority using mutual TLS (mTLS), authenticating
with the server's current ca.crt and ca.key, and requests an automated renewal.
Supports requesting seat quota increases or increments during renewal.
Supports requesting seat quota increases or increments during renewal and reporting active seat usage.
If renewed, writes backup of previous ca.crt and saves new ca.crt in cert_dir.
Returns (success, message, new_ca_cert_pem).
"""
@@ -924,10 +928,15 @@ def renew_ca_cert_mtls(
current_pem = f.read()
info = extract_license_info_from_cert(current_pem)
effective_seats_taken = seats_taken if seats_taken is not None else active_seats
payload_dict = {
"license_id": info.get("license_id"),
"fingerprint": info.get("fingerprint"),
"current_cert": current_pem
"current_cert": current_pem,
"seats_taken": effective_seats_taken,
"active_seats": effective_seats_taken,
"hostname": hostname or "",
"server_fingerprint": server_fingerprint or ""
}
if requested_seats is not None:
payload_dict["requested_seats"] = requested_seats