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 hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import warnings
|
import warnings
|
||||||
|
import threading
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
|
|
||||||
@@ -811,8 +812,46 @@ def check_and_rotate_server_certs(
|
|||||||
return False
|
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):
|
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", {})
|
config = SERVER_STATE.get("config", {})
|
||||||
cert_dir = config.get("cert_dir", "certs")
|
cert_dir = config.get("cert_dir", "certs")
|
||||||
hostnames = [config.get("tcp_host", "0.0.0.0"), "127.0.0.1", "localhost"]
|
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:
|
try:
|
||||||
await asyncio.sleep(interval_seconds)
|
await asyncio.sleep(interval_seconds)
|
||||||
check_and_rotate_server_certs(cert_dir=cert_dir, hostnames=hostnames, threshold_days=threshold_days)
|
check_and_rotate_server_certs(cert_dir=cert_dir, hostnames=hostnames, threshold_days=threshold_days)
|
||||||
|
report_seat_quota_to_authority_sync()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1132,6 +1172,7 @@ def enroll_client(req: ClientEnrollRequest):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
print(f"[+] Successfully enrolled new client: {req.client_id} ({req.hostname}) [Seats: {active_count + 1}/{max_seats}]")
|
print(f"[+] Successfully enrolled new client: {req.client_id} ({req.hostname}) [Seats: {active_count + 1}/{max_seats}]")
|
||||||
|
trigger_async_seat_report()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ca_cert": ca_cert_pem,
|
"ca_cert": ca_cert_pem,
|
||||||
@@ -1231,6 +1272,7 @@ def approve_client(action: ClientActionRequest):
|
|||||||
if affected == 0:
|
if affected == 0:
|
||||||
raise HTTPException(status_code=404, detail="Client ID not found")
|
raise HTTPException(status_code=404, detail="Client ID not found")
|
||||||
print(f"[+] Approved/Reactivated client seat: {action.client_id}")
|
print(f"[+] Approved/Reactivated client seat: {action.client_id}")
|
||||||
|
trigger_async_seat_report()
|
||||||
return {"status": "active", "client_id": action.client_id}
|
return {"status": "active", "client_id": action.client_id}
|
||||||
|
|
||||||
|
|
||||||
@@ -1252,6 +1294,7 @@ def revoke_client(action: ClientActionRequest):
|
|||||||
if affected == 0:
|
if affected == 0:
|
||||||
raise HTTPException(status_code=404, detail="Client ID not found")
|
raise HTTPException(status_code=404, detail="Client ID not found")
|
||||||
print(f"[!] Revoked client license seat: {action.client_id}")
|
print(f"[!] Revoked client license seat: {action.client_id}")
|
||||||
|
trigger_async_seat_report()
|
||||||
return {"status": "revoked", "client_id": action.client_id}
|
return {"status": "revoked", "client_id": action.client_id}
|
||||||
|
|
||||||
|
|
||||||
@@ -1269,6 +1312,7 @@ def delete_client(action: ClientActionRequest):
|
|||||||
if affected == 0:
|
if affected == 0:
|
||||||
raise HTTPException(status_code=404, detail="Client ID not found")
|
raise HTTPException(status_code=404, detail="Client ID not found")
|
||||||
print(f"[+] Removed client forwarder record: {action.client_id}")
|
print(f"[+] Removed client forwarder record: {action.client_id}")
|
||||||
|
trigger_async_seat_report()
|
||||||
return {"status": "deleted", "client_id": action.client_id}
|
return {"status": "deleted", "client_id": action.client_id}
|
||||||
|
|
||||||
|
|
||||||
@@ -1569,11 +1613,29 @@ async def trigger_license_renewal(request: Request):
|
|||||||
config = SERVER_STATE.get("config", {})
|
config = SERVER_STATE.get("config", {})
|
||||||
cert_dir = config.get("cert_dir", "certs")
|
cert_dir = config.get("cert_dir", "certs")
|
||||||
lic_server = config.get("license_server_url", "https://logar.eibl.tech")
|
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(
|
success, msg, new_pem = enrollment.renew_ca_cert_mtls(
|
||||||
cert_dir=cert_dir,
|
cert_dir=cert_dir,
|
||||||
license_server_url=lic_server,
|
license_server_url=lic_server,
|
||||||
requested_seats=requested_seats,
|
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:
|
if not success or not new_pem:
|
||||||
raise HTTPException(status_code=400, detail=msg)
|
raise HTTPException(status_code=400, detail=msg)
|
||||||
@@ -2509,7 +2571,14 @@ def main():
|
|||||||
# Fallback to mTLS HTTP sync
|
# Fallback to mTLS HTTP sync
|
||||||
print(f"[!] WebSocket sync returned: {msg}. Falling back to mTLS renewal...")
|
print(f"[!] WebSocket sync returned: {msg}. Falling back to mTLS renewal...")
|
||||||
http_url = lic_server.replace("wss://", "https://").replace("ws://", "http://").replace("/ws/license", "")
|
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:
|
if not success or not new_pem:
|
||||||
print(f"[-] License sync failed: {msg}")
|
print(f"[-] License sync failed: {msg}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -900,13 +900,17 @@ def renew_ca_cert_mtls(
|
|||||||
upstream_ca_pem: Optional[str] = None,
|
upstream_ca_pem: Optional[str] = None,
|
||||||
requested_seats: Optional[int] = None,
|
requested_seats: Optional[int] = None,
|
||||||
seat_increment: int = 0,
|
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,
|
timeout: int = 15,
|
||||||
verify_tls: bool = True
|
verify_tls: bool = True
|
||||||
) -> Tuple[bool, str, Optional[str]]:
|
) -> Tuple[bool, str, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Connects to the master License Authority using mutual TLS (mTLS), authenticating
|
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.
|
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.
|
If renewed, writes backup of previous ca.crt and saves new ca.crt in cert_dir.
|
||||||
Returns (success, message, new_ca_cert_pem).
|
Returns (success, message, new_ca_cert_pem).
|
||||||
"""
|
"""
|
||||||
@@ -924,10 +928,15 @@ def renew_ca_cert_mtls(
|
|||||||
current_pem = f.read()
|
current_pem = f.read()
|
||||||
info = extract_license_info_from_cert(current_pem)
|
info = extract_license_info_from_cert(current_pem)
|
||||||
|
|
||||||
|
effective_seats_taken = seats_taken if seats_taken is not None else active_seats
|
||||||
payload_dict = {
|
payload_dict = {
|
||||||
"license_id": info.get("license_id"),
|
"license_id": info.get("license_id"),
|
||||||
"fingerprint": info.get("fingerprint"),
|
"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:
|
if requested_seats is not None:
|
||||||
payload_dict["requested_seats"] = requested_seats
|
payload_dict["requested_seats"] = requested_seats
|
||||||
|
|||||||
Reference in New Issue
Block a user