feat(licensing): report active seat quota and usage to upstream license authority
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 21s
This commit is contained in:
+1
-1
Submodule LOGAR-LICENSING updated: 30dfc9f77f...64844808ba
+72
-3
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user