Add signed licensing enforcement and ISO notes

This commit is contained in:
Jino Jose committed 2026-07-08 10:47:49 +05:30
1 parent dee4486826
commit d645249a13
14 files changed
+1526 -103

No files matched your search

+208 -41
View File
@@ -27,6 +27,15 @@ import psutil
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from cezen_license import (
STAGING_MAX_TIER,
collect_entitlement,
evaluate_license,
normalize_tier as license_normalize_tier,
read_json_file,
tier_lte,
write_json_file,
)
# ── Config ────────────────────────────────────────────────────────────────────
@@ -35,6 +44,8 @@ DB_PATH = DATA_DIR / "cezen.db"
SECRET_FILE = DATA_DIR / ".jwt_secret"
BACKUP_DIR = Path(os.environ.get("CEZEN_BACKUP_DIR", str(DATA_DIR.parent / "backups")))
LICENSE_FILE = Path(os.environ.get("CEZEN_LICENSE_JSON", "/opt/cezen/license.json"))
INSTALL_RECORD_FILE = Path(os.environ.get("CEZEN_INSTALL_RECORD_JSON", "/opt/cezen/install-record.json"))
LICENSE_PUBLIC_KEY_FILE = Path(os.environ.get("CEZEN_LICENSE_PUBLIC_KEY", str(Path(__file__).with_name("cezen-license-public.pem"))))
DATA_DIR.mkdir(parents=True, exist_ok=True)
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
@@ -821,23 +832,16 @@ def _setting_value(key: str, default: str = "") -> str:
db.close()
def _normalize_tier(value: str) -> str:
raw = (value or "").strip().lower()
if raw in TIER_MATRIX:
return raw
return TIER_ALIASES.get(raw, "basic")
return license_normalize_tier(value, "basic")
def _license_record() -> dict:
try:
if LICENSE_FILE.exists():
data = json.loads(LICENSE_FILE.read_text())
if isinstance(data, dict):
return data
except Exception:
pass
return {}
return read_json_file(LICENSE_FILE)
def _install_record() -> dict:
return read_json_file(INSTALL_RECORD_FILE)
def _public_license_record(record: dict) -> dict:
public = {k: v for k, v in (record or {}).items() if k != "license_key"}
public = {k: v for k, v in (record or {}).items() if k not in {"license_key", "signature"}}
if record.get("license_key"):
public["license_key_prefix"] = record["license_key"][:12]
public["license_key_present"] = True
@@ -848,26 +852,60 @@ def _public_license_record(record: dict) -> dict:
def _current_tier() -> str:
if CEZEN_TIER:
return _normalize_tier(CEZEN_TIER)
license_tier = (_license_record().get("tier") or "").strip()
if license_tier:
return _normalize_tier(license_tier)
entitlement = _entitlement_summary()
licensed_tier = (entitlement.get("licensed_tier") or "").strip()
if licensed_tier:
return _normalize_tier(licensed_tier)
return _normalize_tier(_setting_value("tier_label", "Basic"))
def _entitlement_summary(feasibility: Optional[dict] = None) -> dict:
feasibility = feasibility or _cached_feasibility()
return collect_entitlement(
TIER_MATRIX,
_license_record(),
_install_record(),
feasibility,
str(LICENSE_PUBLIC_KEY_FILE),
)
def _tier_payload() -> dict:
tier = _current_tier()
entitlement = _entitlement_summary()
tier = entitlement.get("licensed_tier") or _current_tier()
info = TIER_MATRIX[tier]
license_record = _license_record()
return {
"tier": tier,
"label": info["label"],
"locked": bool(CEZEN_TIER or license_record.get("tier")),
"locked": bool(CEZEN_TIER or license_record.get("allowed_tier") or license_record.get("tier")),
"positioning": info["positioning"],
"max_users": info["max_users"],
"features": info["features"],
"features": entitlement.get("effective_features", info["features"]),
"feature_overrides": entitlement.get("feature_overrides", {}),
"provisioned_tier": entitlement.get("provisioned_tier"),
"hardware_recommended_tier": entitlement.get("hardware_recommended_tier"),
"compliance_state": entitlement.get("compliance_state", []),
"license_status": entitlement.get("license_status"),
"override": entitlement.get("override", {}),
"tiers": TIER_MATRIX,
"license_record": _public_license_record(license_record),
}
def _feature_enabled(feature_key: str, feasibility: Optional[dict] = None) -> bool:
entitlement = _entitlement_summary(feasibility)
value = (entitlement.get("effective_features") or {}).get(feature_key)
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() not in {"", "false", "no", "none", "disabled", "optional"}
return bool(value)
def require_feature(feature_key: str):
def dependency(user: dict = Depends(current_user)):
if not _feature_enabled(feature_key):
raise HTTPException(status_code=403, detail=f"Feature '{feature_key}' is not licensed or provisioned")
return user
return dependency
def _readiness_score(feasibility: dict, license_info: dict) -> dict:
features = feasibility.get("features") or {}
recommendation = feasibility.get("recommendation") or {}
@@ -1717,17 +1755,141 @@ async def system_feasibility(admin: dict = Depends(admin_only)):
},
}
def _cached_feasibility() -> dict:
paths = [
Path(os.environ.get("CEZEN_FEASIBILITY_JSON", "")),
DATA_DIR / "feasibility.json",
Path("/opt/cezen/feasibility.json"),
]
for p in paths:
if p and str(p) != "." and p.exists():
try:
return json.loads(p.read_text())
except Exception:
pass
metrics_now = collect_metrics()
gpu_vram = metrics_now.get("gpu_mem_total_gb") or 0
return {
"schema": "cezen.feasibility.cached-fallback.v1",
"generated_at": utcnow(),
"recommendation": {
"recommended_tier": STAGING_MAX_TIER,
"recommended_profile": "core",
"estimated_concurrent_users": "1-2",
"notes": [],
},
"features": {
"ollama_gpu": gpu_vram >= 8,
"vllm": gpu_vram >= 24,
"fine_tuning_qlora": gpu_vram >= 24,
"distributed_training": False,
},
}
@app.get("/api/license")
async def license_info(user: dict = Depends(current_user)):
return _tier_payload()
payload = _tier_payload()
payload["entitlement"] = _entitlement_summary()
return payload
@app.post("/api/license/upload")
async def upload_license(body: dict, request: Request, admin: dict = Depends(admin_only)):
raw_license = (body.get("license_json") or "").strip()
raw_override = (body.get("override_json") or "").strip()
if not raw_license:
raise HTTPException(400, "license_json required")
try:
license_record = json.loads(raw_license)
if not isinstance(license_record, dict):
raise ValueError("license_json must be a JSON object")
except Exception as exc:
raise HTTPException(400, f"Invalid license_json: {exc}")
if raw_override:
try:
override_record = json.loads(raw_override)
if not isinstance(override_record, dict):
raise ValueError("override_json must be a JSON object")
except Exception as exc:
raise HTTPException(400, f"Invalid override_json: {exc}")
write_json_file(Path("/opt/cezen/license.override.json"), override_record)
else:
Path("/opt/cezen/license.override.json").unlink(missing_ok=True)
write_json_file(LICENSE_FILE, license_record)
entitlement = _entitlement_summary()
db = get_db()
audit(
db,
admin["sub"],
admin["username"],
"license_upload",
f"status={entitlement.get('license_status')} licensed_tier={entitlement.get('licensed_tier')}",
request.client.host if request.client else "",
"success" if entitlement.get("license_valid") else "failure",
)
db.commit()
db.close()
return {"ok": True, "entitlement": entitlement}
@app.post("/api/license/reverify")
async def reverify_license(request: Request, admin: dict = Depends(admin_only)):
entitlement = _entitlement_summary()
db = get_db()
audit(
db,
admin["sub"],
admin["username"],
"license_reverify",
f"status={entitlement.get('license_status')}",
request.client.host if request.client else "",
"success" if entitlement.get("license_valid") else "failure",
)
db.commit()
db.close()
return {"ok": True, "entitlement": entitlement}
@app.post("/api/license/reconcile")
async def reconcile_license(request: Request, admin: dict = Depends(admin_only)):
entitlement = _entitlement_summary()
record = _install_record()
requested_tier = entitlement.get("licensed_tier") or record.get("provisioned_tier") or STAGING_MAX_TIER
pending_roles = []
if not tier_lte(requested_tier, record.get("provisioned_tier") or STAGING_MAX_TIER):
if requested_tier in {"pro", "max"} and not (record.get("components") or {}).get("vllm", False):
pending_roles.append("vllm")
if requested_tier == "max":
for role in ("mlflow", "minio", "k3s"):
if not (record.get("components") or {}).get(role, False):
pending_roles.append(role)
reconcile_record = {
"schema": "cezen.reconcile_request.v1",
"requested_at": utcnow(),
"requested_by": admin["username"],
"target_tier": requested_tier,
"pending_roles": pending_roles,
"status": "pending" if pending_roles else "noop",
}
write_json_file(Path("/opt/cezen/reconcile-request.json"), reconcile_record)
db = get_db()
audit(
db,
admin["sub"],
admin["username"],
"license_reconcile",
f"target_tier={requested_tier} pending_roles={','.join(pending_roles) or 'none'}",
request.client.host if request.client else "",
)
db.commit()
db.close()
return {"ok": True, "reconcile": reconcile_record, "entitlement": entitlement}
@app.get("/api/system/readiness-report")
async def readiness_report(admin: dict = Depends(admin_only)):
feasibility = await system_feasibility(admin)
license_payload = _tier_payload()
entitlement = _entitlement_summary(feasibility)
readiness = _readiness_score(feasibility, license_payload)
recommended_tier = (feasibility.get("recommendation") or {}).get("recommended_tier", "starter")
current_rank = list(TIER_MATRIX.keys()).index(license_payload["tier"])
current_rank = list(TIER_MATRIX.keys()).index(entitlement.get("licensed_tier") or license_payload["tier"])
recommended_rank = list(TIER_MATRIX.keys()).index(_normalize_tier(recommended_tier))
commercial_fit = "matched" if current_rank >= recommended_rank else "license_upgrade_recommended"
return {
@@ -1735,11 +1897,13 @@ async def readiness_report(admin: dict = Depends(admin_only)):
"generated_at": utcnow(),
"customer_mode": "software_only" if (feasibility.get("features") or {}).get("software_only", True) else "appliance",
"license": license_payload,
"entitlement": entitlement,
"feasibility": feasibility,
"readiness": readiness,
"commercial_fit": {
"status": commercial_fit,
"current_tier": license_payload["tier"],
"current_tier": entitlement.get("licensed_tier") or license_payload["tier"],
"provisioned_tier": entitlement.get("provisioned_tier"),
"recommended_tier": _normalize_tier(recommended_tier),
"note": "Current license covers the recommended deployment." if commercial_fit == "matched" else "Quote a higher tier or reduce enabled features for this hardware.",
},
@@ -1829,8 +1993,9 @@ async def get_branding():
db.close()
result = {r["key"]: r["value"] for r in rows}
license_record = _license_record()
entitlement = _entitlement_summary()
# If Cezen has locked the tier via env var, override whatever is in DB
if CEZEN_TIER or license_record.get("tier"):
if CEZEN_TIER or license_record.get("allowed_tier") or license_record.get("tier"):
tier = _current_tier()
result["tier_label"] = TIER_MATRIX[tier]["label"]
result["tier_slug"] = tier
@@ -1842,13 +2007,15 @@ async def get_branding():
result["license_customer_id"] = license_record.get("customer_id", "")
result["license_key_prefix"] = (license_record.get("license_key", "") or "")[:12]
result["license_support_until"] = license_record.get("support_until", "")
result["license_status"] = entitlement.get("license_status", "missing")
result["provisioned_tier"] = entitlement.get("provisioned_tier", "")
return result
@app.put("/api/settings/branding")
async def update_branding(body: dict, admin: dict = Depends(admin_only)):
# tier_label is always excluded from customer-editable fields when locked
allowed = {"org_name","stack_name","logo_url","accent_color","footer_text","support_email"}
if not CEZEN_TIER:
if not (CEZEN_TIER or _license_record().get("allowed_tier") or _license_record().get("tier")):
# Only allow tier changes when NOT locked by env var (dev/demo mode)
allowed.add("tier_label")
db = get_db()
@@ -2149,7 +2316,7 @@ def _count_rows(path: Path, orig_name: str) -> int:
return 0
@app.post("/api/training/datasets")
async def upload_dataset(file: UploadFile = File(...), admin: dict = Depends(admin_only)):
async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
"""Upload a JSONL or CSV dataset for fine-tuning."""
allowed_ext = {".jsonl", ".json", ".csv"}
orig = file.filename or "dataset"
@@ -2180,7 +2347,7 @@ async def upload_dataset(file: UploadFile = File(...), admin: dict = Depends(adm
return {"id": row_id, "filename": orig, "size_bytes": len(data), "row_count": row_count}
@app.get("/api/training/datasets")
async def list_datasets(admin: dict = Depends(admin_only)):
async def list_datasets(_licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
rows = db.execute(
"SELECT id, orig_name, size_bytes, row_count, uploaded_at FROM training_datasets ORDER BY uploaded_at DESC"
@@ -2189,7 +2356,7 @@ async def list_datasets(admin: dict = Depends(admin_only)):
return {"datasets": [dict(r) for r in rows]}
@app.delete("/api/training/datasets/{dataset_id}")
async def delete_dataset(dataset_id: int, admin: dict = Depends(admin_only)):
async def delete_dataset(dataset_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
row = db.execute("SELECT * FROM training_datasets WHERE id=?", (dataset_id,)).fetchone()
if not row:
@@ -2212,7 +2379,7 @@ async def delete_dataset(dataset_id: int, admin: dict = Depends(admin_only)):
return {"ok": True}
@app.post("/api/training/jobs")
async def launch_job(body: dict, admin: dict = Depends(admin_only)):
async def launch_job(body: dict, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
"""Launch a QLoRA fine-tuning job as a background subprocess."""
required = {"name", "base_model", "dataset_id"}
if not required.issubset(body):
@@ -2284,7 +2451,7 @@ async def launch_job(body: dict, admin: dict = Depends(admin_only)):
return {"job_id": job_id, "status": "running"}
@app.get("/api/training/jobs")
async def list_jobs(admin: dict = Depends(admin_only)):
async def list_jobs(_licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
rows = db.execute(
"""SELECT j.id, j.name, j.base_model, j.status, j.config_json,
@@ -2303,7 +2470,7 @@ async def list_jobs(admin: dict = Depends(admin_only)):
return {"jobs": result}
@app.get("/api/training/jobs/{job_id}")
async def get_job(job_id: int, admin: dict = Depends(admin_only), tail: int = 200):
async def get_job(job_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only), tail: int = 200):
db = get_db()
row = db.execute(
"""SELECT j.*, d.orig_name as dataset_name
@@ -2338,7 +2505,7 @@ async def get_job(job_id: int, admin: dict = Depends(admin_only), tail: int = 20
return item
@app.delete("/api/training/jobs/{job_id}")
async def cancel_job(job_id: int, admin: dict = Depends(admin_only)):
async def cancel_job(job_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
row = db.execute("SELECT * FROM training_jobs WHERE id=?", (job_id,)).fetchone()
if not row:
@@ -2723,7 +2890,7 @@ class BenchmarkRun(BaseModel):
max_tokens: int = 256
@app.post("/api/benchmark/run")
async def run_benchmark(body: BenchmarkRun, admin: dict = Depends(admin_only)):
async def run_benchmark(body: BenchmarkRun, _licensed: dict = Depends(require_feature("gpu_inference")), admin: dict = Depends(admin_only)):
"""
Runs each prompt against each model and returns timing + response.
Calls Ollama /api/generate for each (model, prompt) pair sequentially.
@@ -5171,14 +5338,14 @@ async def run_workflow_api(workflow_id: str, body: dict, user: dict = Depends(cu
return result
@app.get("/api/connectors")
async def list_connectors(user: dict = Depends(current_user)):
async def list_connectors(user: dict = Depends(require_feature("connectors"))):
db = get_db()
rows = db.execute("SELECT * FROM connectors ORDER BY updated_at DESC").fetchall()
db.close()
return [_connector_from_row(r) for r in rows]
@app.put("/api/connectors")
async def save_connectors(body: list[dict], user: dict = Depends(current_user)):
async def save_connectors(body: list[dict], user: dict = Depends(require_feature("connectors"))):
now = utcnow()
db = get_db()
seen = []
@@ -5284,7 +5451,7 @@ def _connector_db_stats(cfg: dict) -> tuple[int, int]:
raise ValueError(f"Unsupported database connector type: {db_type}")
@app.post("/api/connectors/{connector_id}/sync")
async def sync_connector(connector_id: str, user: dict = Depends(current_user)):
async def sync_connector(connector_id: str, user: dict = Depends(require_feature("connectors"))):
db = get_db()
row = db.execute("SELECT * FROM connectors WHERE id=?", (connector_id,)).fetchone()
if not row:
@@ -5327,14 +5494,14 @@ async def sync_connector(connector_id: str, user: dict = Depends(current_user)):
return {"ok": True, "files": stats.get("files", 0), "rowsRead": stats.get("rowsRead", 0), "status": status}
@app.get("/api/connectors/log")
async def connector_log(limit: int = 30, user: dict = Depends(current_user)):
async def connector_log(limit: int = 30, user: dict = Depends(require_feature("connectors"))):
db = get_db()
rows = db.execute("SELECT level,msg,ts FROM connector_log ORDER BY id DESC LIMIT ?", (limit,)).fetchall()
db.close()
return {"lines": [dict(r) for r in reversed(rows)]}
@app.get("/api/router/rules")
async def get_router_rules(user: dict = Depends(current_user)):
async def get_router_rules(user: dict = Depends(require_feature("model_router"))):
db = get_db()
rows = db.execute("SELECT * FROM router_rules ORDER BY priority, id").fetchall()
db.close()
@@ -5348,7 +5515,7 @@ async def get_router_rules(user: dict = Depends(current_user)):
return {"routes": routes}
@app.put("/api/router/rules")
async def save_router_rules(body: dict, admin: dict = Depends(admin_only)):
async def save_router_rules(body: dict, _licensed: dict = Depends(require_feature("model_router")), admin: dict = Depends(admin_only)):
routes = body.get("routes") or []
db = get_db()
db.execute("DELETE FROM router_rules")
@@ -5364,7 +5531,7 @@ async def save_router_rules(body: dict, admin: dict = Depends(admin_only)):
return {"ok": True, "routes": len(routes)}
@app.put("/api/router/fallback")
async def save_router_fallback(body: dict, admin: dict = Depends(admin_only)):
async def save_router_fallback(body: dict, _licensed: dict = Depends(require_feature("model_router")), admin: dict = Depends(admin_only)):
db = get_db()
for key, value in body.items():
if key == "cloud_key" and value:
@@ -5449,7 +5616,7 @@ async def rag_quality_warnings(admin: dict = Depends(admin_only)):
return {"warnings": warnings}
@app.post("/api/meeting/analyse")
async def analyse_meeting(body: dict, user: dict = Depends(current_user)):
async def analyse_meeting(body: dict, user: dict = Depends(require_feature("meeting_assistant"))):
transcript = (body.get("transcript") or "").strip()
if not transcript:
raise HTTPException(400, "transcript required")
@@ -5466,7 +5633,7 @@ async def analyse_meeting(body: dict, user: dict = Depends(current_user)):
return result
@app.post("/api/meeting/process")
async def process_meeting(file: UploadFile = File(...), meta: str = Form("{}"), user: dict = Depends(current_user)):
async def process_meeting(file: UploadFile = File(...), meta: str = Form("{}"), user: dict = Depends(require_feature("meeting_assistant"))):
try:
meta_obj = json.loads(meta) if isinstance(meta, str) else {}
except Exception: