feat(ui): display licensed feature capabilities in Sub-CA & Licensing tab
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m19s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m19s
This commit is contained in:
+88
-11
@@ -1188,11 +1188,13 @@ def export_license_request_lic(config: Dict[str, Any]) -> Tuple[Dict[str, Any],
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_feature_licensed(feature: str, db_path: Optional[str] = None) -> bool:
|
def get_active_licensed_features(db_path: Optional[str] = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Checks whether a specific enterprise capability (e.g. 'enterprise_db') is unlocked
|
Returns the list of all active enterprise capability identifiers
|
||||||
either via custom X.509 certificate extensions or the persistent license configuration.
|
authorized either via X.509 certificate extensions or the persistent database license config.
|
||||||
"""
|
"""
|
||||||
|
features = set()
|
||||||
|
|
||||||
# 1. Check in-memory / active Subordinate CA certificate
|
# 1. Check in-memory / active Subordinate CA certificate
|
||||||
ca_pem = SERVER_STATE.get("ca_cert_pem")
|
ca_pem = SERVER_STATE.get("ca_cert_pem")
|
||||||
if not ca_pem:
|
if not ca_pem:
|
||||||
@@ -1208,9 +1210,9 @@ def is_feature_licensed(feature: str, db_path: Optional[str] = None) -> bool:
|
|||||||
if ca_pem:
|
if ca_pem:
|
||||||
try:
|
try:
|
||||||
info = enrollment.extract_license_info_from_cert(ca_pem)
|
info = enrollment.extract_license_info_from_cert(ca_pem)
|
||||||
cert_features = info.get("features", [])
|
for feat in info.get("features", []):
|
||||||
if feature in cert_features:
|
if feat:
|
||||||
return True
|
features.add(str(feat).strip())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1230,14 +1232,24 @@ def is_feature_licensed(feature: str, db_path: Optional[str] = None) -> bool:
|
|||||||
feat_list = json.loads(feat_raw)
|
feat_list = json.loads(feat_raw)
|
||||||
else:
|
else:
|
||||||
feat_list = [f.strip() for f in feat_raw.split(",") if f.strip()]
|
feat_list = [f.strip() for f in feat_raw.split(",") if f.strip()]
|
||||||
if feature in feat_list:
|
for f_item in feat_list:
|
||||||
return True
|
if f_item:
|
||||||
elif isinstance(feat_raw, list) and feature in feat_raw:
|
features.add(str(f_item).strip())
|
||||||
return True
|
elif isinstance(feat_raw, list):
|
||||||
|
for f_item in feat_raw:
|
||||||
|
if f_item:
|
||||||
|
features.add(str(f_item).strip())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
return sorted(list(features))
|
||||||
|
|
||||||
|
|
||||||
|
def is_feature_licensed(feature: str, db_path: Optional[str] = None) -> bool:
|
||||||
|
"""Checks whether a specific enterprise capability (e.g. 'enterprise_db') is authorized."""
|
||||||
|
if not feature:
|
||||||
return False
|
return False
|
||||||
|
return feature in get_active_licensed_features(db_path)
|
||||||
|
|
||||||
|
|
||||||
def get_server_public_key() -> Optional[pgpy.PGPKey]:
|
def get_server_public_key() -> Optional[pgpy.PGPKey]:
|
||||||
@@ -2863,7 +2875,8 @@ def get_license_summary() -> Dict[str, Any]:
|
|||||||
"days_remaining": info.get("days_remaining", 0),
|
"days_remaining": info.get("days_remaining", 0),
|
||||||
"fingerprint": info.get("fingerprint", "N/A"),
|
"fingerprint": info.get("fingerprint", "N/A"),
|
||||||
"csr_exists": csr_exists,
|
"csr_exists": csr_exists,
|
||||||
"server_fingerprint": config.get("server_fingerprint", "N/A")
|
"server_fingerprint": config.get("server_fingerprint", "N/A"),
|
||||||
|
"features": get_active_licensed_features(db_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3396,6 +3409,55 @@ def serve_webui(request: Request = None):
|
|||||||
pg_disabled_attr = "" if is_db_unlocked else "disabled"
|
pg_disabled_attr = "" if is_db_unlocked else "disabled"
|
||||||
pg_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
pg_label_style = "" if is_db_unlocked else "opacity: 0.55; cursor: not-allowed;"
|
||||||
|
|
||||||
|
active_features = summary.get("features", [])
|
||||||
|
feature_cards = []
|
||||||
|
if active_features:
|
||||||
|
for feat in active_features:
|
||||||
|
if feat == "enterprise_db":
|
||||||
|
feature_cards.append("""
|
||||||
|
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid rgba(16, 185, 129, 0.35); display: flex; align-items: flex-start; gap: 14px;">
|
||||||
|
<div style="font-size: 22px; line-height: 1;">🐘</div>
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 3px;">
|
||||||
|
<strong style="color: #34d399; font-size: 14px;">enterprise_db</strong>
|
||||||
|
<span class="badge badge-active" style="font-size: 10px;">AUTHORIZED</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: var(--text-muted); line-height: 1.4;">
|
||||||
|
<strong>Enterprise Database & High-Throughput Engine:</strong> Unlocks external PostgreSQL storage clustering, customizable in-memory RAM buffering with batch flushing, and OpenPGP zero-knowledge data-at-rest encryption.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
else:
|
||||||
|
esc_feat = html.escape(str(feat))
|
||||||
|
feat_title = html.escape(str(feat).replace("_", " ").title())
|
||||||
|
feature_cards.append(f"""
|
||||||
|
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid rgba(16, 185, 129, 0.35); display: flex; align-items: flex-start; gap: 14px;">
|
||||||
|
<div style="font-size: 22px; line-height: 1;">✨</div>
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 3px;">
|
||||||
|
<strong style="color: #34d399; font-size: 14px;">{esc_feat}</strong>
|
||||||
|
<span class="badge badge-active" style="font-size: 10px;">AUTHORIZED</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: var(--text-muted); line-height: 1.4;">
|
||||||
|
<strong>{feat_title}:</strong> Advanced capability authorized by your license certificate.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
else:
|
||||||
|
feature_cards.append("""
|
||||||
|
<div style="background: #0f172a; padding: 16px 20px; border-radius: 8px; border: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; color: #94a3b8; font-size: 14px; margin-bottom: 2px;">No Enterprise Capabilities Active</div>
|
||||||
|
<div style="font-size: 12px; color: var(--text-muted);">Standard Community Edition is running. External PostgreSQL pooling, RAM queue buffering, and zero-knowledge encryption require an <code>enterprise_db</code> capability package.</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="openImportLicModal()" class="btn btn-accent" style="padding: 6px 12px; font-size: 11px;">📥 Import Enterprise .lic</button>
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
|
||||||
|
features_block_html = "".join(feature_cards)
|
||||||
|
|
||||||
ent_community_notice = "" if is_db_unlocked else f"""
|
ent_community_notice = "" if is_db_unlocked else f"""
|
||||||
<div style="margin-bottom: 20px; background: rgba(59, 130, 246, 0.08); border: 1px solid rgba(59, 130, 246, 0.25); border-radius: 8px; padding: 12px 16px; font-size: 13px; color: #94a3b8; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
|
<div style="margin-bottom: 20px; background: rgba(59, 130, 246, 0.08); border: 1px solid rgba(59, 130, 246, 0.25); border-radius: 8px; padding: 12px 16px; font-size: 13px; color: #94a3b8; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
|
||||||
<div>
|
<div>
|
||||||
@@ -3998,6 +4060,21 @@ def serve_webui(request: Request = None):
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Licensed Capabilities & Feature Modules -->
|
||||||
|
<div class="card" style="margin-top: 20px;">
|
||||||
|
<div class="card-header">
|
||||||
|
<div>
|
||||||
|
<span class="card-title">✨ Licensed Capabilities & Feature Modules</span>
|
||||||
|
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">Features authorized by your subordinate CA license package and upstream authority</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge {'badge-active' if active_features else 'badge-os'}">{len(active_features)} ACTIVE FEATURES</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 6px;">
|
||||||
|
{features_block_html}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{db_tab_pane_html}
|
{db_tab_pane_html}
|
||||||
|
|||||||
Reference in New Issue
Block a user