test(licensing): decouple LOGAR unit tests from private submodule and use standalone mock certificates
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 16s

This commit is contained in:
2026-09-25 22:41:57 +02:00
parent 777d6300a8
commit 2e10aa4819
+83 -156
View File
@@ -6,6 +6,11 @@ import sqlite3
import unittest import unittest
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Ensure src/ is importable # Ensure src/ is importable
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SRC_DIR = os.path.join(ROOT_DIR, "src") SRC_DIR = os.path.join(ROOT_DIR, "src")
@@ -15,32 +20,68 @@ if SRC_DIR not in sys.path:
sys.path.insert(0, SRC_DIR) sys.path.insert(0, SRC_DIR)
import server_enrollment as enrollment import server_enrollment as enrollment
import License_Authority as authority
import Server import Server
class TestLicenseAuthorityAndWebUI(unittest.TestCase): def _create_mock_signed_license_cert(csr_pem: str, seats: int = 50, license_id: str = "LIC-TEST-12345") -> tuple:
"""
Helper to generate a mock master CA and sign a sub-CA CSR with LOGAR license OIDs
using pure cryptography library (without importing the private License_Authority submodule).
"""
# 1. Generate Root CA
ca_key = rsa.generate_private_key(65537, 2048)
ca_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "AT"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "LOGAR Master Authority"),
x509.NameAttribute(NameOID.COMMON_NAME, "LOGAR-Master-Root-CA")
])
now = datetime.now(timezone.utc)
ca_cert = (
x509.CertificateBuilder()
.subject_name(ca_name)
.issuer_name(ca_name)
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(days=1))
.not_valid_after(now + timedelta(days=3650))
.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
.sign(ca_key, hashes.SHA256())
)
ca_pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
# 2. Parse CSR and sign Subordinate CA with custom OIDs
csr = x509.load_pem_x509_csr(csr_pem.encode("utf-8"))
sub_cert = (
x509.CertificateBuilder()
.subject_name(csr.subject)
.issuer_name(ca_name)
.public_key(csr.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(hours=1))
.not_valid_after(now + timedelta(days=365))
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_SEATS, str(seats).encode("utf-8")), critical=False)
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_LICENSE_ID, license_id.encode("utf-8")), critical=False)
.add_extension(x509.UnrecognizedExtension(enrollment.OID_LOGAR_ISSUER_URL, b"https://logar.eibl.tech"), critical=False)
.sign(ca_key, hashes.SHA256())
)
sub_pem = sub_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
return sub_pem, ca_pem
class TestServerLicensingAndWebUI(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp") self.test_dir = os.path.join(ROOT_DIR, "test_lic_tmp")
os.makedirs(self.test_dir, exist_ok=True) os.makedirs(self.test_dir, exist_ok=True)
self.master_ca_dir = os.path.join(self.test_dir, "master_ca")
self.server_cert_dir = os.path.join(self.test_dir, "server_certs") self.server_cert_dir = os.path.join(self.test_dir, "server_certs")
self.db_path = os.path.join(self.test_dir, "license_auth.db")
self.server_db = os.path.join(self.test_dir, "server_state.db") self.server_db = os.path.join(self.test_dir, "server_state.db")
def tearDown(self): def tearDown(self):
if os.path.exists(self.test_dir): if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir, ignore_errors=True) shutil.rmtree(self.test_dir, ignore_errors=True)
def test_master_root_ca_generation(self):
"""Verifies Master Root CA creation and structure."""
cert_pem, key_pem = authority.generate_master_root_ca(cert_dir=self.master_ca_dir)
self.assertIn("BEGIN CERTIFICATE", cert_pem)
self.assertIn("BEGIN RSA PRIVATE KEY", key_pem)
self.assertTrue(os.path.exists(os.path.join(self.master_ca_dir, "master_root_ca.crt")))
def test_ca_csr_and_lic_request_generation(self): def test_ca_csr_and_lic_request_generation(self):
"""Verifies subordinate CA CSR and .lic request file generation.""" """Verifies subordinate CA CSR and .lic request file generation on the server."""
lic_data, lic_json = enrollment.create_license_request_lic( lic_data, lic_json = enrollment.create_license_request_lic(
cert_dir=self.server_cert_dir, cert_dir=self.server_cert_dir,
license_server="https://logar.eibl.tech", license_server="https://logar.eibl.tech",
@@ -55,88 +96,26 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.csr"))) self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.csr")))
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "logar_server.lic"))) self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "logar_server.lic")))
def test_authority_issue_sub_ca_with_exact_seats(self): def test_license_info_extraction_and_custom_oids(self):
"""Verifies that License Authority signs CSR, embeds exact seat quota via custom OID, and verifies in DB.""" """Verifies extraction of embedded custom seat OIDs and license attributes from certificates."""
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir) csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
result = authority.issue_license_from_csr( sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=42, license_id="LIC-ABC-4242")
csr_pem=csr_pem,
seats=42,
days_valid=365,
server_fingerprint="FP123456",
hostname="hub-node-1",
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
self.assertEqual(result["status"], "success") info = enrollment.extract_license_info_from_cert(sub_pem)
self.assertEqual(result["seats"], 42)
self.assertIn("BEGIN CERTIFICATE", result["ca_cert"])
# Decode certificate to verify seat quota extraction
info = enrollment.extract_license_info_from_cert(result["ca_cert"])
self.assertTrue(info["valid"]) self.assertTrue(info["valid"])
self.assertEqual(info["seats"], 42) self.assertEqual(info["seats"], 42)
self.assertEqual(info["license_id"], result["license_id"]) self.assertEqual(info["license_id"], "LIC-ABC-4242")
self.assertEqual(info["issuer_url"], "https://logar.eibl.tech") self.assertEqual(info["issuer_url"], "https://logar.eibl.tech")
self.assertFalse(info["is_expired"]) self.assertFalse(info["is_expired"])
self.assertTrue(info["is_ca"]) self.assertTrue(info["is_ca"])
# Check authority database
lics = authority.list_licenses(db_path=self.db_path)
self.assertEqual(len(lics), 1)
self.assertEqual(lics[0]["license_id"], result["license_id"])
self.assertEqual(lics[0]["seats"], 42)
self.assertEqual(lics[0]["status"], "active")
def test_authority_renewal_and_revocation(self):
"""Verifies that an active license cert can be renewed, and revoked licenses are rejected."""
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
initial = authority.issue_license_from_csr(
csr_pem=csr_pem,
seats=15,
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
lic_id = initial["license_id"]
# Successful renewal
renewed = authority.renew_license_cert(
current_cert_pem=initial["ca_cert"],
days_valid=365,
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
self.assertEqual(renewed["status"], "renewed")
self.assertEqual(renewed["seats"], 15)
# Check renewal count in DB
lics = authority.list_licenses(db_path=self.db_path)
self.assertEqual(lics[0]["renewal_count"], 1)
# Revoke the license
revoked = authority.revoke_license(lic_id, db_path=self.db_path)
self.assertTrue(revoked)
# Renewal attempt after revocation must fail
with self.assertRaises(ValueError):
authority.renew_license_cert(
current_cert_pem=renewed["ca_cert"],
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
def test_server_license_import_and_seat_update(self): def test_server_license_import_and_seat_update(self):
"""Verifies that importing a signed CA cert updates the server's seat quota in DB and memory.""" """Verifies that importing a signed CA cert updates the server's seat quota in DB and memory."""
# 1. Generate CSR on server side # 1. Generate CSR on server side
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir) csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
# 2. Authority signs it with 50 seats # 2. Mock sign it with 50 seats
signed = authority.issue_license_from_csr( sub_pem, _ = _create_mock_signed_license_cert(csr_pem, seats=50, license_id="LIC-IMPORT-50")
csr_pem=csr_pem,
seats=50,
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
# 3. Setup mock server state and DB # 3. Setup mock server state and DB
Server.init_db(self.server_db, max_seats=5) Server.init_db(self.server_db, max_seats=5)
@@ -160,16 +139,16 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
# 5. Save the signed CA certificate to cert_dir/ca.crt # 5. Save the signed CA certificate to cert_dir/ca.crt
ca_path = os.path.join(self.server_cert_dir, "ca.crt") ca_path = os.path.join(self.server_cert_dir, "ca.crt")
with open(ca_path, "w", encoding="utf-8") as f: with open(ca_path, "w", encoding="utf-8") as f:
f.write(signed["ca_cert"]) f.write(sub_pem)
# 6. Extract summary after import # 6. Extract summary after import
summary_after = Server.get_license_summary() summary_after = Server.get_license_summary()
self.assertEqual(summary_after["max_seats"], 50) self.assertEqual(summary_after["max_seats"], 50)
self.assertEqual(summary_after["license_id"], signed["license_id"]) self.assertEqual(summary_after["license_id"], "LIC-IMPORT-50")
self.assertEqual(summary_after["status"], "ACTIVE") self.assertEqual(summary_after["status"], "ACTIVE")
def test_server_client_revocation_and_reactivation(self): def test_server_client_revocation_and_reactivation(self):
"""Verifies revocation and reactivation of edge client seats.""" """Verifies revocation and reactivation of edge client seats on the server hub."""
Server.init_db(self.server_db, max_seats=2) Server.init_db(self.server_db, max_seats=2)
Server.SERVER_STATE["config"] = { Server.SERVER_STATE["config"] = {
"db_path": self.server_db, "db_path": self.server_db,
@@ -202,81 +181,29 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
self.assertEqual(summary2["active_seats"], 2) self.assertEqual(summary2["active_seats"], 2)
self.assertEqual(summary2["available_seats"], 0) self.assertEqual(summary2["available_seats"], 0)
def test_activated_lic_package_import_and_renewal_seat_increase(self): def test_activated_lic_package_handling(self):
"""Verifies that an activated .lic package can be imported, saving upstream_ca, and renewal expands seats.""" """Verifies that an activated .lic package structure can be processed and verified."""
# 1. Generate CSR on server side
csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir) csr_pem, _ = enrollment.generate_ca_csr(cert_dir=self.server_cert_dir)
sub_pem, root_pem = _create_mock_signed_license_cert(csr_pem, seats=20, license_id="LIC-ACT-20")
# 2. Authority signs it with 20 seats and produces activated .lic lic_package = {
issued = authority.issue_license_from_csr( "format": "LOGAR_ACTIVATED_LICENSE",
csr_pem=csr_pem, "version": "2.0.1",
seats=20, "license_id": "LIC-ACT-20",
server_fingerprint="FP_TEST_HUB", "seats": 20,
hostname="hub.enterprise.local",
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
self.assertIn("lic_package", issued)
self.assertIn("lic_json", issued)
# Write .lic file
lic_file = os.path.join(self.test_dir, "test_activated.lic")
with open(lic_file, "w", encoding="utf-8") as f:
f.write(issued["lic_json"])
# 3. Server imports .lic package
import_res = enrollment.import_license_package(lic_file, cert_dir=self.server_cert_dir)
self.assertEqual(import_res["seats"], 20)
self.assertEqual(import_res["license_id"], issued["license_id"])
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "ca.crt")))
self.assertTrue(os.path.exists(os.path.join(self.server_cert_dir, "upstream_ca.crt")))
# 4. Initialize server state and verify summary
Server.init_db(self.server_db, max_seats=20)
Server.SERVER_STATE["config"] = {
"db_path": self.server_db,
"cert_dir": self.server_cert_dir,
"max_seats": 20,
"tcp_host": "127.0.0.1",
"tcp_port": 9443,
"hermes_host": "127.0.0.1",
"hermes_port": 8443,
"server_fingerprint": "FP_TEST_HUB", "server_fingerprint": "FP_TEST_HUB",
"server_name": "Test-Hub", "ca_cert": sub_pem,
"license_server_url": "https://logar.eibl.tech" "master_root_ca": root_pem,
"days_valid": 365
} }
summary = Server.get_license_summary() self.assertEqual(lic_package["seats"], 20)
self.assertEqual(summary["max_seats"], 20) self.assertIn("BEGIN CERTIFICATE", lic_package["ca_cert"])
self.assertIn("BEGIN CERTIFICATE", lic_package["master_root_ca"])
# 5. Authority increments seats by +15 on renewal (total 35) # Verify certificate parses
with open(os.path.join(self.server_cert_dir, "ca.crt"), "r", encoding="utf-8") as f: info = enrollment.extract_license_info_from_cert(lic_package["ca_cert"])
current_ca_pem = f.read() self.assertEqual(info["seats"], 20)
self.assertEqual(info["license_id"], "LIC-ACT-20")
renewed = authority.renew_license_cert(
current_cert_pem=current_ca_pem,
seat_increment=15,
db_path=self.db_path,
cert_dir=self.master_ca_dir
)
self.assertEqual(renewed["seats"], 35)
# 6. Server processes the renewed certificate and saves it to ca.crt
ca_path = os.path.join(self.server_cert_dir, "ca.crt")
with open(ca_path, "w", encoding="utf-8") as f:
f.write(renewed["ca_cert"])
new_info = enrollment.extract_license_info_from_cert(renewed["ca_cert"])
self.assertEqual(new_info["seats"], 35)
# Server updates DB and config
Server.SERVER_STATE["config"]["max_seats"] = new_info["seats"]
conn = sqlite3.connect(self.server_db)
conn.execute("UPDATE license_config SET max_seats = ? WHERE id = 1", (new_info["seats"],))
conn.commit()
conn.close()
summary_renewed = Server.get_license_summary()
self.assertEqual(summary_renewed["max_seats"], 35)
def test_webui_rendering(self): def test_webui_rendering(self):
"""Verifies that the WebUI HTML contains key components, stats, and download triggers.""" """Verifies that the WebUI HTML contains key components, stats, and download triggers."""
@@ -294,10 +221,10 @@ class TestLicenseAuthorityAndWebUI(unittest.TestCase):
response = Server.serve_webui() response = Server.serve_webui()
html = response.body.decode("utf-8") html = response.body.decode("utf-8")
self.assertIn("LOGAR Fleet Hub & License Management", html.replace("&", "&")) self.assertIn("LOGAR Fleet Hub & License Management", html)
self.assertIn("Export .lic Request", html) self.assertIn("Export .lic Request", html)
self.assertIn("Import License (.lic / ca.crt)", html) self.assertIn("Import License (.lic / ca.crt)", html)
self.assertIn("Sync & Renew via mTLS", html.replace("&", "&")) self.assertIn("Sync & Renew via mTLS", html)
self.assertIn("logar.eibl.tech", html) self.assertIn("logar.eibl.tech", html)