feat(hub): enforce license requirement for fleet auto-updates while keeping server upstream check free
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m4s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m4s
This commit is contained in:
+31
-3
@@ -1497,6 +1497,13 @@ def sync_upstream_packages_api(auth_user: str = Depends(require_admin_auth)):
|
|||||||
"""
|
"""
|
||||||
Downloads latest client binary packages from upstream release assets to local Hub storage.
|
Downloads latest client binary packages from upstream release assets to local Hub storage.
|
||||||
"""
|
"""
|
||||||
|
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||||
|
if not is_feature_licensed("auto_update", db_path):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Fleet package acquisition and distribution requires an active license with the 'auto_update' capability."
|
||||||
|
)
|
||||||
|
|
||||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||||
results = mgr.sync_upstream_packages(force=True)
|
results = mgr.sync_upstream_packages(force=True)
|
||||||
return {
|
return {
|
||||||
@@ -1513,6 +1520,13 @@ def update_server_update_config(req: ServerUpdateConfigRequest, auth_user: str =
|
|||||||
"""
|
"""
|
||||||
Updates fleet auto-update settings (on/off switch and automatic vs manual acquisition mode).
|
Updates fleet auto-update settings (on/off switch and automatic vs manual acquisition mode).
|
||||||
"""
|
"""
|
||||||
|
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||||
|
if not is_feature_licensed("auto_update", db_path):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Fleet auto-update settings configuration requires an active license with the 'auto_update' capability."
|
||||||
|
)
|
||||||
|
|
||||||
config = SERVER_STATE.get("config", {})
|
config = SERVER_STATE.get("config", {})
|
||||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||||
|
|
||||||
@@ -1562,12 +1576,10 @@ def check_client_update(
|
|||||||
"""
|
"""
|
||||||
Invoked by edge forwarders to check whether a newer client binary package is available.
|
Invoked by edge forwarders to check whether a newer client binary package is available.
|
||||||
"""
|
"""
|
||||||
import os as _os
|
|
||||||
target_os = os_name or os or "windows"
|
target_os = os_name or os or "windows"
|
||||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||||
|
|
||||||
# Record check timestamp in database
|
# Record check timestamp in database
|
||||||
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
|
||||||
if target_os:
|
if target_os:
|
||||||
now_sql = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
now_sql = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
try:
|
try:
|
||||||
@@ -1581,6 +1593,15 @@ def check_client_update(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if not is_feature_licensed("auto_update", db_path):
|
||||||
|
return {
|
||||||
|
"update_available": False,
|
||||||
|
"latest_version": LOGAR_VERSION,
|
||||||
|
"current_version": current_version,
|
||||||
|
"reason": "Fleet auto-update is not authorized by the server license."
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||||
return mgr.check_client_update(client_id=client_id, os_type=target_os, current_version=current_version, arch=arch)
|
return mgr.check_client_update(client_id=client_id, os_type=target_os, current_version=current_version, arch=arch)
|
||||||
|
|
||||||
|
|
||||||
@@ -1597,6 +1618,13 @@ def download_client_update_package(
|
|||||||
import os as _os
|
import os as _os
|
||||||
import email.utils
|
import email.utils
|
||||||
target_os = os_name or os or "windows"
|
target_os = os_name or os or "windows"
|
||||||
|
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||||
|
if not is_feature_licensed("auto_update", db_path):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Client binary package fleet distribution requires an active license with the 'auto_update' capability."
|
||||||
|
)
|
||||||
|
|
||||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||||
pkg_tuple = mgr.get_client_package_path(os_type=target_os)
|
pkg_tuple = mgr.get_client_package_path(os_type=target_os)
|
||||||
if not pkg_tuple:
|
if not pkg_tuple:
|
||||||
|
|||||||
+12
-10
@@ -680,6 +680,8 @@ class WebUIRenderer:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{auto_update_lock_banner}
|
||||||
|
|
||||||
<!-- Fleet Auto-Update & Acquisition Policy Form -->
|
<!-- Fleet Auto-Update & Acquisition Policy Form -->
|
||||||
<div class="card" style="margin-bottom: 24px;">
|
<div class="card" style="margin-bottom: 24px;">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -688,43 +690,43 @@ class WebUIRenderer:
|
|||||||
<div class="subtitle" style="margin-top: 4px;">Configure hot-upgrade execution and choose how client binary images are acquired & hosted</div>
|
<div class="subtitle" style="margin-top: 4px;">Configure hot-upgrade execution and choose how client binary images are acquired & hosted</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
{('<span class="badge badge-active" style="font-size: 11px;">🔄 FLEET AUTO-UPDATE LICENSED</span>' if is_auto_update_unlocked else '<span class="badge" style="background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); font-size: 11px;">COMMUNITY / UNRESTRICTED</span>')}
|
{('<span class="badge badge-active" style="font-size: 11px;">🔄 FLEET AUTO-UPDATE LICENSED</span>' if is_auto_update_unlocked else '<span class="badge" style="background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); font-size: 11px;">🔒 ENTERPRISE LICENSE REQUIRED</span>')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onsubmit="saveUpdateConfig(event)">
|
<form onsubmit="saveUpdateConfig(event)">
|
||||||
<!-- Switch 1: Fleet Auto-Update Service (On / Off) -->
|
<!-- Switch 1: Fleet Auto-Update Service (On / Off) -->
|
||||||
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px;">
|
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px; {upload_container_style}">
|
||||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
||||||
<span>⚡</span> Fleet Auto-Update Service
|
<span>⚡</span> Fleet Auto-Update Service
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 6px;">
|
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 6px;">
|
||||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||||
<input type="radio" name="autoUpdateEnabledRadio" id="autoUpdateOn" value="1" {'checked' if auto_update_enabled else ''}>
|
<input type="radio" name="autoUpdateEnabledRadio" id="autoUpdateOn" value="1" {'checked' if (auto_update_enabled and is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||||
<span>🟢 <strong>Enabled (ON)</strong> <span style="color: var(--text-muted); font-size: 12px;">— Edge forwarders query this Hub and auto hot-upgrade</span></span>
|
<span>🟢 <strong>Enabled (ON)</strong> <span style="color: var(--text-muted); font-size: 12px;">— Edge forwarders query this Hub and auto hot-upgrade</span></span>
|
||||||
</label>
|
</label>
|
||||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||||
<input type="radio" name="autoUpdateEnabledRadio" id="autoUpdateOff" value="0" {'checked' if not auto_update_enabled else ''}>
|
<input type="radio" name="autoUpdateEnabledRadio" id="autoUpdateOff" value="0" {'checked' if (not auto_update_enabled or not is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||||
<span>🔴 <strong>Disabled (OFF)</strong> <span style="color: var(--text-muted); font-size: 12px;">— Suspend fleet hot-upgrades (clients stay on current version)</span></span>
|
<span>🔴 <strong>Disabled (OFF)</strong> <span style="color: var(--text-muted); font-size: 12px;">— Suspend fleet hot-upgrades (clients stay on current version)</span></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Switch 2: Image Acquisition & Hosting Mode (Auto Fetch vs Manual Provided) -->
|
<!-- Switch 2: Image Acquisition & Hosting Mode (Auto Fetch vs Manual Provided) -->
|
||||||
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px;">
|
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px; {upload_container_style}">
|
||||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
||||||
<span>📥</span> Image Acquisition & Hosting Mode
|
<span>📥</span> Image Acquisition & Hosting Mode
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px;">
|
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px;">
|
||||||
<label style="display: inline-flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
<label style="display: inline-flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||||
<input type="radio" name="updateModeRadio" id="modeAuto" value="auto" {'checked' if update_mode == 'auto' else ''}>
|
<input type="radio" name="updateModeRadio" id="modeAuto" value="auto" {'checked' if (update_mode == 'auto' and is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600; font-size: 13px; color: #34d399;">🌐 Automatic (Hub Downloads & Hosts Upstream Images)</div>
|
<div style="font-weight: 600; font-size: 13px; color: #34d399;">🌐 Automatic (Hub Downloads & Hosts Upstream Images)</div>
|
||||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">The LOGAR Hub connects to the upstream release repository, automatically downloads Windows & Linux client binaries (.exe, .bin), caches and hosts them locally for the fleet.</div>
|
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">The LOGAR Hub connects to the upstream release repository, automatically downloads Windows & Linux client binaries (.exe, .bin), caches and hosts them locally for the fleet.</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label style="display: inline-flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
<label style="display: inline-flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||||
<input type="radio" name="updateModeRadio" id="modeManual" value="manual" {'checked' if update_mode == 'manual' else ''}>
|
<input type="radio" name="updateModeRadio" id="modeManual" value="manual" {'checked' if (update_mode == 'manual' or not is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600; font-size: 13px; color: #f59e0b;">📁 Manually Provided (Upload Only)</div>
|
<div style="font-weight: 600; font-size: 13px; color: #f59e0b;">📁 Manually Provided (Upload Only)</div>
|
||||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">The LOGAR Hub will not download packages from the internet. Binary packages must be manually uploaded by an administrator below.</div>
|
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">The LOGAR Hub will not download packages from the internet. Binary packages must be manually uploaded by an administrator below.</div>
|
||||||
@@ -733,13 +735,13 @@ class WebUIRenderer:
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label class="form-label" style="font-size: 12px;">Upstream Release API Endpoint</label>
|
<label class="form-label" style="font-size: 12px;">Upstream Release API Endpoint</label>
|
||||||
<input type="text" id="upstreamReleaseUrl" class="form-control" value="{html.escape(str(upstream_url))}" placeholder="https://gitea.eibl.tech/api/v1/repos/me0nline/LOGAR/releases/latest">
|
<input type="text" id="upstreamReleaseUrl" class="form-control" value="{html.escape(str(upstream_url))}" placeholder="https://gitea.eibl.tech/api/v1/repos/me0nline/LOGAR/releases/latest" {upload_disabled_attr}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<button type="submit" class="btn btn-primary">💾 Save Update Settings</button>
|
<button type="submit" class="btn btn-primary" {upload_disabled_attr}>💾 Save Update Settings</button>
|
||||||
<button type="button" onclick="syncUpstreamPackagesNow()" class="btn btn-accent">🔄 Sync & Download Upstream Images Now</button>
|
<button type="button" onclick="syncUpstreamPackagesNow()" class="btn btn-accent" {upload_disabled_attr}>🔄 Sync & Download Upstream Images Now</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -323,12 +323,12 @@ class TestServerLicensingAndWebUI(unittest.TestCase):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# WebUI should render update controls and community status
|
# WebUI should render locked banner and disabled update controls
|
||||||
resp = Server.serve_webui()
|
resp = Server.serve_webui()
|
||||||
html = resp.body.decode("utf-8")
|
html = resp.body.decode("utf-8")
|
||||||
self.assertIn("Fleet Distribution & Auto-Update Policy", html)
|
self.assertIn("Fleet Auto-Update is Inactive & Locked", html)
|
||||||
self.assertIn("COMMUNITY / UNRESTRICTED", html)
|
self.assertIn("ENTERPRISE LICENSE REQUIRED", html)
|
||||||
self.assertIn("autoUpdateEnabledRadio", html)
|
self.assertIn("disabled", html)
|
||||||
|
|
||||||
def test_json_lic_package_import(self):
|
def test_json_lic_package_import(self):
|
||||||
"""Verifies importing an activated .lic JSON package via /api/license/import."""
|
"""Verifies importing an activated .lic JSON package via /api/license/import."""
|
||||||
|
|||||||
@@ -130,14 +130,14 @@ class TestUpdateSystem(unittest.TestCase):
|
|||||||
f.write(b"WIN_CLIENT_V0360")
|
f.write(b"WIN_CLIENT_V0360")
|
||||||
self.update_mgr.register_client_package("windows", dummy_pkg, version="0.3.6.0")
|
self.update_mgr.register_client_package("windows", dummy_pkg, version="0.3.6.0")
|
||||||
|
|
||||||
# 3. Client update check endpoint (Allowed regardless of license)
|
# 3. Client update check endpoint (Unlicensed blocks client auto_update)
|
||||||
chk_unlicensed = Server.check_client_update(
|
chk_unlicensed = Server.check_client_update(
|
||||||
client_id="win-client-01",
|
client_id="win-client-01",
|
||||||
os="windows",
|
os="windows",
|
||||||
current_version="0.3.5.1"
|
current_version="0.3.5.1"
|
||||||
)
|
)
|
||||||
self.assertTrue(chk_unlicensed["update_available"])
|
self.assertFalse(chk_unlicensed["update_available"])
|
||||||
self.assertEqual(chk_unlicensed["latest_version"], "0.3.6.0")
|
self.assertIn("not authorized", chk_unlicensed.get("reason", ""))
|
||||||
|
|
||||||
# 4. Enable auto_update feature flag in license config
|
# 4. Enable auto_update feature flag in license config
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|||||||
Reference in New Issue
Block a user