windows installer fixes
This commit is contained in:
@@ -50,6 +50,7 @@ Type: filesandordirs; Name: "{commonappdata}\LOGAR"
|
|||||||
[Code]
|
[Code]
|
||||||
var
|
var
|
||||||
DataDirPage: TInputDirWizardPage;
|
DataDirPage: TInputDirWizardPage;
|
||||||
|
ConfigFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
procedure InitializeWizard;
|
procedure InitializeWizard;
|
||||||
begin
|
begin
|
||||||
@@ -64,6 +65,16 @@ begin
|
|||||||
);
|
);
|
||||||
DataDirPage.Add('Data Directory:');
|
DataDirPage.Add('Data Directory:');
|
||||||
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
|
DataDirPage.Values[0] := ExpandConstant('{commonappdata}\LOGAR');
|
||||||
|
|
||||||
|
ConfigFilePage := CreateInputFilePage(
|
||||||
|
DataDirPage.ID,
|
||||||
|
'Import Client Configuration (Optional)',
|
||||||
|
'Do you have a pre-exported client_config.json profile to import?',
|
||||||
|
'Select your client_config.json profile exported from the LOGAR Server Hub.'#13#10#13#10 +
|
||||||
|
'If selected, this configuration will be copied into the data directory and used for automated server connection and enrollment.'#13#10#13#10 +
|
||||||
|
'You can also leave this blank to place client_config.json in the data directory manually later.'
|
||||||
|
);
|
||||||
|
ConfigFilePage.Add('Client Configuration (client_config.json):', 'JSON Files (*.json)|*.json|All Files (*.*)|*.*', '.json');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function GetDataDir(Param: String): String;
|
function GetDataDir(Param: String): String;
|
||||||
@@ -73,11 +84,25 @@ begin
|
|||||||
Result := ExpandConstant('{commonappdata}\LOGAR');
|
Result := ExpandConstant('{commonappdata}\LOGAR');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function GetConfigFile(Param: String): String;
|
||||||
|
begin
|
||||||
|
Result := ConfigFilePage.Values[0];
|
||||||
|
end;
|
||||||
|
|
||||||
procedure CurStepChanged(CurStep: TSetupStep);
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
var
|
||||||
|
CfgPath, DestCfg: String;
|
||||||
begin
|
begin
|
||||||
if CurStep = ssPostInstall then
|
if CurStep = ssPostInstall then
|
||||||
begin
|
begin
|
||||||
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
|
RegWriteStringValue(HKEY_LOCAL_MACHINE, 'Software\LOGAR\LOGAR-Client', 'DataDir', GetDataDir(''));
|
||||||
|
|
||||||
|
CfgPath := GetConfigFile('');
|
||||||
|
if (CfgPath <> '') and FileExists(CfgPath) then
|
||||||
|
begin
|
||||||
|
DestCfg := GetDataDir('') + '\client_config.json';
|
||||||
|
FileCopy(CfgPath, DestCfg, False);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ VIAddVersionKey "FileVersion" "0.3.2.0"
|
|||||||
VIAddVersionKey "ProductVersion" "0.3.2.0"
|
VIAddVersionKey "ProductVersion" "0.3.2.0"
|
||||||
|
|
||||||
Var DATADIR
|
Var DATADIR
|
||||||
|
Var CFGFILE
|
||||||
|
Var CfgDialog
|
||||||
|
Var CfgFileInput
|
||||||
|
Var CfgBrowseBtn
|
||||||
|
|
||||||
# --- UI Pages ---
|
# --- UI Pages ---
|
||||||
!define MUI_ABORTWARNING
|
!define MUI_ABORTWARNING
|
||||||
@@ -40,6 +44,9 @@ Var DATADIR
|
|||||||
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
!define MUI_DIRECTORYPAGE_VARIABLE $DATADIR
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
# Custom Page: Config File Selection
|
||||||
|
Page custom ConfigPageCreate ConfigPageLeave
|
||||||
|
|
||||||
# Instfiles Page
|
# Instfiles Page
|
||||||
!insertmacro MUI_PAGE_INSTFILES
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
@@ -57,8 +64,44 @@ Var DATADIR
|
|||||||
Function .onInit
|
Function .onInit
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
StrCpy $DATADIR "$APPDATA\LOGAR"
|
StrCpy $DATADIR "$APPDATA\LOGAR"
|
||||||
|
StrCpy $CFGFILE ""
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function ConfigPageCreate
|
||||||
|
nsDialogs::Create 1018
|
||||||
|
Pop $CfgDialog
|
||||||
|
${If} $CfgDialog == error
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
!insertmacro MUI_HEADER_TEXT "Import Client Configuration (Optional)" "Choose a client_config.json profile exported from LOGAR Server."
|
||||||
|
|
||||||
|
${NSD_CreateLabel} 0 0 100% 30u "If you have exported a client_config.json profile bundle from your LOGAR Server dashboard, select it below to configure the forwarder immediately.$\r$\n$\r$\nLeave this field blank to configure later."
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${NSD_CreateText} 0 38u 75% 12u "$CFGFILE"
|
||||||
|
Pop $CfgFileInput
|
||||||
|
|
||||||
|
${NSD_CreateButton} 78% 37u 22% 14u "Browse..."
|
||||||
|
Pop $CfgBrowseBtn
|
||||||
|
${NSD_OnClick} $CfgBrowseBtn OnBrowseCfg
|
||||||
|
|
||||||
|
nsDialogs::Show
|
||||||
|
Function
|
||||||
|
|
||||||
|
Function OnBrowseCfg
|
||||||
|
nsDialogs::SelectFileDialog open "" "LOGAR Client Profiles (*.json)|*.json|All Files (*.*)|*.*"
|
||||||
|
Pop $0
|
||||||
|
${If} $0 != ""
|
||||||
|
StrCpy $CFGFILE $0
|
||||||
|
${NSD_SetText} $CfgFileInput $CFGFILE
|
||||||
|
${EndIf}
|
||||||
|
Function
|
||||||
|
|
||||||
|
Function ConfigPageLeave
|
||||||
|
${NSD_GetText} $CfgFileInput $CFGFILE
|
||||||
|
Function
|
||||||
|
|
||||||
Section "Install"
|
Section "Install"
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
@@ -69,6 +112,12 @@ Section "Install"
|
|||||||
CreateDirectory "$DATADIR"
|
CreateDirectory "$DATADIR"
|
||||||
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
ExecWait 'icacls "$DATADIR" /grant "*S-1-5-32-545:(OI)(CI)M" /T /Q'
|
||||||
|
|
||||||
|
# If client config file was selected, copy to data directory
|
||||||
|
${If} $CFGFILE != ""
|
||||||
|
${AndIf} ${FileExists} "$CFGFILE"
|
||||||
|
CopyFiles /SILENT "$CFGFILE" "$DATADIR\client_config.json"
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
# Install and configure service via NSSM
|
# Install and configure service via NSSM
|
||||||
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Client "$INSTDIR\Win_Client.exe"'
|
ExecWait '"$INSTDIR\nssm.exe" install LOGAR_Client "$INSTDIR\Win_Client.exe"'
|
||||||
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppDirectory "$INSTDIR"'
|
ExecWait '"$INSTDIR\nssm.exe" set LOGAR_Client AppDirectory "$INSTDIR"'
|
||||||
|
|||||||
@@ -15,10 +15,24 @@ BUILD_TEMP = os.path.join(ROOT_DIR, "build_temp")
|
|||||||
|
|
||||||
def clean_and_prep():
|
def clean_and_prep():
|
||||||
if os.path.exists(DIST_DIR):
|
if os.path.exists(DIST_DIR):
|
||||||
shutil.rmtree(DIST_DIR)
|
for item in os.listdir(DIST_DIR):
|
||||||
os.makedirs(DIST_DIR, exist_ok=True)
|
if item == "runtime":
|
||||||
|
continue
|
||||||
|
item_path = os.path.join(DIST_DIR, item)
|
||||||
|
try:
|
||||||
|
if os.path.isdir(item_path):
|
||||||
|
shutil.rmtree(item_path)
|
||||||
|
else:
|
||||||
|
os.remove(item_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
os.makedirs(DIST_DIR, exist_ok=True)
|
||||||
if os.path.exists(BUILD_TEMP):
|
if os.path.exists(BUILD_TEMP):
|
||||||
shutil.rmtree(BUILD_TEMP)
|
try:
|
||||||
|
shutil.rmtree(BUILD_TEMP)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
os.makedirs(BUILD_TEMP, exist_ok=True)
|
os.makedirs(BUILD_TEMP, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import warnings
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
# Ensure script directory and repo root are in sys.path
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if SCRIPT_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, SCRIPT_DIR)
|
||||||
|
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
||||||
|
if REPO_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, REPO_ROOT)
|
||||||
|
|
||||||
# Suppress cryptography / pgpy deprecation notices
|
# Suppress cryptography / pgpy deprecation notices
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
|||||||
+42
-7
@@ -17,6 +17,14 @@ 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
|
||||||
|
|
||||||
|
# Ensure script directory and repo root are in sys.path
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if SCRIPT_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, SCRIPT_DIR)
|
||||||
|
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
||||||
|
if REPO_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, REPO_ROOT)
|
||||||
|
|
||||||
# Suppress cryptography / pgpy deprecation notices for a clean terminal output
|
# Suppress cryptography / pgpy deprecation notices for a clean terminal output
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
@@ -910,6 +918,12 @@ def init_mtls_server_context(cert_dir: str = "certs") -> ssl.SSLContext:
|
|||||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||||
ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
||||||
ctx.load_verify_locations(cafile=ca_file)
|
ctx.load_verify_locations(cafile=ca_file)
|
||||||
|
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
|
||||||
|
if os.path.exists(upstream_file):
|
||||||
|
try:
|
||||||
|
ctx.load_verify_locations(cafile=upstream_file)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
|
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||||
return ctx
|
return ctx
|
||||||
@@ -926,6 +940,12 @@ def reload_mtls_context(ssl_ctx: ssl.SSLContext, cert_dir: str = "certs"):
|
|||||||
|
|
||||||
ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
||||||
ssl_ctx.load_verify_locations(cafile=ca_file)
|
ssl_ctx.load_verify_locations(cafile=ca_file)
|
||||||
|
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
|
||||||
|
if os.path.exists(upstream_file):
|
||||||
|
try:
|
||||||
|
ssl_ctx.load_verify_locations(cafile=upstream_file)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def check_and_rotate_server_certs(
|
def check_and_rotate_server_certs(
|
||||||
@@ -1347,6 +1367,18 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
|
|||||||
conn.close()
|
conn.close()
|
||||||
raise HTTPException(status_code=500, detail="Root CA not loaded on server")
|
raise HTTPException(status_code=500, detail="Root CA not loaded on server")
|
||||||
|
|
||||||
|
# Ensure full trust chain (subordinate CA + upstream root CA if present) is delivered to edge clients
|
||||||
|
cert_dir = SERVER_STATE.get("config", {}).get("cert_dir", "certs")
|
||||||
|
upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt")
|
||||||
|
if os.path.exists(upstream_ca_path):
|
||||||
|
try:
|
||||||
|
with open(upstream_ca_path, "r", encoding="utf-8") as f:
|
||||||
|
upstream_pem = f.read().strip()
|
||||||
|
if upstream_pem and upstream_pem not in ca_cert_pem:
|
||||||
|
ca_cert_pem = ca_cert_pem.strip() + "\n" + upstream_pem
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 2. Check if client_id already registered
|
# 2. Check if client_id already registered
|
||||||
c.execute("SELECT status FROM clients WHERE client_id = ?", (req.client_id,))
|
c.execute("SELECT status FROM clients WHERE client_id = ?", (req.client_id,))
|
||||||
client_row = c.fetchone()
|
client_row = c.fetchone()
|
||||||
@@ -1766,14 +1798,17 @@ async def import_signed_license(req: Request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status_code=400, detail="Invalid JSON body")
|
raise HTTPException(status_code=400, detail="Invalid JSON body")
|
||||||
|
|
||||||
raw_input = body.get("ca_cert") or body.get("lic") or body.get("raw") or body.get("license") or body.get("license_file")
|
if isinstance(body, dict):
|
||||||
if not raw_input:
|
if "master_root_ca" in body or "upstream_ca" in body:
|
||||||
raise HTTPException(status_code=400, detail="Missing 'lic' or 'ca_cert' payload in request")
|
raw_text = json.dumps(body)
|
||||||
|
elif "lic" in body:
|
||||||
if isinstance(raw_input, dict):
|
raw_text = json.dumps(body["lic"]) if isinstance(body["lic"], dict) else str(body["lic"])
|
||||||
raw_text = json.dumps(raw_input)
|
elif "ca_cert" in body and not body.get("master_root_ca"):
|
||||||
|
raw_text = str(body["ca_cert"])
|
||||||
|
else:
|
||||||
|
raw_text = json.dumps(body)
|
||||||
else:
|
else:
|
||||||
raw_text = str(raw_input)
|
raw_text = str(body)
|
||||||
|
|
||||||
config = SERVER_STATE.get("config", {})
|
config = SERVER_STATE.get("config", {})
|
||||||
cert_dir = config.get("cert_dir", "certs")
|
cert_dir = config.get("cert_dir", "certs")
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import warnings
|
|||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
# Ensure script directory and repo root are in sys.path
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if SCRIPT_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, SCRIPT_DIR)
|
||||||
|
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
||||||
|
if REPO_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, REPO_ROOT)
|
||||||
|
|
||||||
# Suppress cryptography / pgpy deprecation notices
|
# Suppress cryptography / pgpy deprecation notices
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
|||||||
@@ -251,8 +251,24 @@ def generate_server_cert_if_needed(
|
|||||||
encryption_algorithm=serialization.NoEncryption()
|
encryption_algorithm=serialization.NoEncryption()
|
||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
|
|
||||||
|
# Bundle subordinate CA and upstream CA into server.crt for complete TLS handshake chain
|
||||||
|
full_server_cert_pem = server_cert_pem
|
||||||
|
ca_cert_path = os.path.join(cert_dir, "ca.crt")
|
||||||
|
if os.path.exists(ca_cert_path):
|
||||||
|
with open(ca_cert_path, "r", encoding="utf-8") as f:
|
||||||
|
ca_pem = f.read().strip()
|
||||||
|
if ca_pem and ca_pem not in full_server_cert_pem:
|
||||||
|
full_server_cert_pem = full_server_cert_pem.strip() + "\n" + ca_pem
|
||||||
|
|
||||||
|
upstream_ca_path = os.path.join(cert_dir, "upstream_ca.crt")
|
||||||
|
if os.path.exists(upstream_ca_path):
|
||||||
|
with open(upstream_ca_path, "r", encoding="utf-8") as f:
|
||||||
|
upstream_pem = f.read().strip()
|
||||||
|
if upstream_pem and upstream_pem not in full_server_cert_pem:
|
||||||
|
full_server_cert_pem = full_server_cert_pem.strip() + "\n" + upstream_pem
|
||||||
|
|
||||||
with open(server_cert_path, "w", encoding="utf-8") as f:
|
with open(server_cert_path, "w", encoding="utf-8") as f:
|
||||||
f.write(server_cert_pem)
|
f.write(full_server_cert_pem)
|
||||||
with open(server_key_path, "w", encoding="utf-8") as f:
|
with open(server_key_path, "w", encoding="utf-8") as f:
|
||||||
f.write(server_key_pem)
|
f.write(server_key_pem)
|
||||||
|
|
||||||
|
|||||||
+76
-1
@@ -8,6 +8,7 @@ import struct
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
import shutil
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
@@ -51,9 +52,84 @@ def run_tests():
|
|||||||
print(f"[OK] Fingerprint verified: {client_conf['server_fingerprint']}")
|
print(f"[OK] Fingerprint verified: {client_conf['server_fingerprint']}")
|
||||||
|
|
||||||
cert_dir = os.path.join(ROOT_DIR, "test_pipeline_certs")
|
cert_dir = os.path.join(ROOT_DIR, "test_pipeline_certs")
|
||||||
|
shutil.rmtree(cert_dir, ignore_errors=True)
|
||||||
os.makedirs(cert_dir, exist_ok=True)
|
os.makedirs(cert_dir, exist_ok=True)
|
||||||
client_id = "test-edge-node.corp.internal"
|
client_id = "test-edge-node.corp.internal"
|
||||||
enrollment_secret = server_conf.get("enrollment_secret") or client_conf.get("enrollment_secret")
|
enrollment_secret = server_conf.get("enrollment_secret") or client_conf.get("enrollment_secret")
|
||||||
|
auth_token = server_conf.get("auth_token", "")
|
||||||
|
|
||||||
|
print("\n=== [1.5] Testing Server License Activation via Web API (/api/license/import) ===")
|
||||||
|
import server_enrollment
|
||||||
|
|
||||||
|
# 1. Check current license status
|
||||||
|
req_status_init = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/status")
|
||||||
|
with urllib.request.urlopen(req_status_init, timeout=5) as resp:
|
||||||
|
init_lic_stat = json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
if init_lic_stat.get("status") == "UNLICENSED":
|
||||||
|
unlicensed_enroll_payload = {
|
||||||
|
"client_id": client_id,
|
||||||
|
"hostname": client_id,
|
||||||
|
"os": "linux",
|
||||||
|
"enrollment_secret": enrollment_secret
|
||||||
|
}
|
||||||
|
req_unlicensed = urllib.request.Request(
|
||||||
|
f"http://{HERMES_HOST}:{HERMES_PORT}/api/client/enroll",
|
||||||
|
data=json.dumps(unlicensed_enroll_payload).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req_unlicensed, timeout=5):
|
||||||
|
assert False, "Expected HTTP 403 on unlicensed server enrollment"
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
assert e.code == 403, f"Expected HTTP 403 on unlicensed server, got {e.code}"
|
||||||
|
print("[OK] Unlicensed server correctly rejected edge client enrollment.")
|
||||||
|
|
||||||
|
# 2. Fetch CA CSR from Server
|
||||||
|
req_csr = urllib.request.Request(
|
||||||
|
f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/csr",
|
||||||
|
headers={"Authorization": f"Bearer {auth_token}"}
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req_csr, timeout=5) as resp:
|
||||||
|
assert resp.status == 200
|
||||||
|
csr_data = json.loads(resp.read().decode("utf-8"))
|
||||||
|
csr_pem = csr_data["csr"]
|
||||||
|
print("[OK] Fetched subordinate CA CSR from server.")
|
||||||
|
|
||||||
|
# 3. Generate test master CA and sign subordinate CA with 10 seats
|
||||||
|
master_ca_dir = os.path.join(ROOT_DIR, "test_pipeline_master_ca")
|
||||||
|
master_cert, master_key, master_pem, master_key_pem = server_enrollment.generate_ca_if_needed(cert_dir=master_ca_dir, common_name="LOGAR-Master-Root-CA")
|
||||||
|
sub_pem = server_enrollment.sign_ca_csr_with_seats(
|
||||||
|
csr_pem=csr_pem,
|
||||||
|
master_ca_cert_pem=master_pem,
|
||||||
|
master_ca_key_pem=master_key_pem,
|
||||||
|
license_id="LIC-TEST-PIPELINE-10",
|
||||||
|
seats=10,
|
||||||
|
days_valid=365
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Import signed license package to server
|
||||||
|
import_payload = {"ca_cert": sub_pem, "master_root_ca": master_pem}
|
||||||
|
req_import = urllib.request.Request(
|
||||||
|
f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/import",
|
||||||
|
data=json.dumps(import_payload).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json", "Authorization": f"Bearer {auth_token}"}
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req_import, timeout=5) as resp:
|
||||||
|
assert resp.status == 200
|
||||||
|
import_res = json.loads(resp.read().decode("utf-8"))
|
||||||
|
assert import_res["status"] == "success"
|
||||||
|
assert import_res["seats"] == 10
|
||||||
|
print("[OK] Successfully imported signed license (.lic). Server activated with 10 seats.")
|
||||||
|
|
||||||
|
# 5. Verify license status
|
||||||
|
req_status = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/license/status")
|
||||||
|
with urllib.request.urlopen(req_status, timeout=5) as resp:
|
||||||
|
assert resp.status == 200
|
||||||
|
lic_stat = json.loads(resp.read().decode("utf-8"))
|
||||||
|
assert lic_stat["status"] == "ACTIVE"
|
||||||
|
assert lic_stat["max_seats"] == 10
|
||||||
|
print("[OK] Verified server license status is ACTIVE.")
|
||||||
|
|
||||||
print("\n=== [2] Testing Client Dynamic PKI Enrollment API (/api/client/enroll) ===")
|
print("\n=== [2] Testing Client Dynamic PKI Enrollment API (/api/client/enroll) ===")
|
||||||
enroll_url = f"http://{HERMES_HOST}:{HERMES_PORT}/api/client/enroll"
|
enroll_url = f"http://{HERMES_HOST}:{HERMES_PORT}/api/client/enroll"
|
||||||
@@ -244,7 +320,6 @@ def run_tests():
|
|||||||
linux_logs = get_recent_linux_logs(hours=24)
|
linux_logs = get_recent_linux_logs(hours=24)
|
||||||
print(f"[Linux_Client] Successfully queried Linux logs: {len(linux_logs)} candidate entries.")
|
print(f"[Linux_Client] Successfully queried Linux logs: {len(linux_logs)} candidate entries.")
|
||||||
|
|
||||||
import shutil
|
|
||||||
if os.path.exists(cert_dir):
|
if os.path.exists(cert_dir):
|
||||||
shutil.rmtree(cert_dir, ignore_errors=True)
|
shutil.rmtree(cert_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
test_cert_dir = "test_certs_enroll"
|
test_cert_dir = "test_certs_enroll"
|
||||||
try:
|
try:
|
||||||
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
|
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
|
||||||
Server.SERVER_STATE["config"] = {"db_path": self.test_db}
|
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": test_cert_dir}
|
||||||
Server.SERVER_STATE["ca_cert"] = ca_cert
|
Server.SERVER_STATE["ca_cert"] = ca_cert
|
||||||
Server.SERVER_STATE["ca_key"] = ca_key
|
Server.SERVER_STATE["ca_key"] = ca_key
|
||||||
Server.SERVER_STATE["ca_cert_pem"] = ca_pem
|
Server.SERVER_STATE["ca_cert_pem"] = ca_pem
|
||||||
@@ -246,7 +246,7 @@ class TestServerComponent(unittest.TestCase):
|
|||||||
resp1 = Server.enroll_client(req1)
|
resp1 = Server.enroll_client(req1)
|
||||||
self.assertIn("client_cert", resp1)
|
self.assertIn("client_cert", resp1)
|
||||||
self.assertIn("client_key", resp1)
|
self.assertIn("client_key", resp1)
|
||||||
self.assertEqual(resp1["ca_cert"], ca_pem)
|
self.assertTrue(resp1["ca_cert"].strip().startswith(ca_pem.strip()))
|
||||||
|
|
||||||
# 3. Valid enrollment for client 2
|
# 3. Valid enrollment for client 2
|
||||||
req2 = Server.ClientEnrollRequest(
|
req2 = Server.ClientEnrollRequest(
|
||||||
|
|||||||
Reference in New Issue
Block a user