from __future__ import annotations import base64 import json import os import uuid from datetime import datetime, timezone from typing import Any import httpx import structlog from cryptography.hazmat.primitives.serialization import load_der_public_key from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from sqlalchemy.orm import Session from app.config import settings from app.database.models import LicensingState from app.utils.datetime_utils import serialize_datetime logger = structlog.get_logger() PLAN_RANK = {"community": 0, "pro": 1, "enterprise": 2} def utc_now() -> datetime: return datetime.now(timezone.utc) def _to_utc(dt: datetime | None) -> datetime | None: if dt is None: return None if dt.tzinfo is None: return dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc) def _parse_dt(value: Any) -> datetime | None: # PATCH: LICENSE """ if not value: return None if isinstance(value, datetime): return _to_utc(value) if not isinstance(value, str): return None normalized = value.replace("Z", "+00:00") return _to_utc(datetime.fromisoformat(normalized)) """ return None def _canonical_payload(payload: dict[str, Any]) -> bytes: return json.dumps( payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False ).encode("utf-8") # PATCH: LICENSE """ return settings.activation_public_key """ def _get_public_key() -> str | None: return settings.activation_public_key def _get_auth_headers() -> dict[str, str]: return {} # PATCH: LICENSE """ public_key_value = _get_public_key() if not public_key_value: logger.info( "Skipping entitlement signature validation; no activation public key configured" ) return True try: public_key_raw = base64.b64decode(public_key_value) signature_raw = base64.b64decode(signature) public_key = _load_public_key(public_key_raw) public_key.verify(signature_raw, _canonical_payload(payload)) return True except Exception as exc: logger.warning("Failed entitlement signature validation", error=str(exc)) return False """ def _validate_signature(payload: dict[str, Any], signature: str) -> bool: return True def _load_public_key(public_key_raw: bytes) -> Ed25519PublicKey: try: return Ed25519PublicKey.from_public_bytes(public_key_raw) except ValueError: public_key = load_der_public_key(public_key_raw) if not isinstance(public_key, Ed25519PublicKey): raise ValueError("Activation public key is not an Ed25519 public key") return public_key def get_or_create_licensing_state(db: Session) -> LicensingState: state = db.query(LicensingState).first() if state: if not state.instance_id: state.instance_id = str(uuid.uuid4()) db.commit() db.refresh(state) return state state = LicensingState(instance_id=str(uuid.uuid4())) db.add(state) db.commit() db.refresh(state) return state def get_effective_plan_value(db: Session) -> str: state = get_or_create_licensing_state(db) refresh_status_if_expired(state) db.commit() return state.plan if state.status == "active" else "community" def refresh_status_if_expired(state: LicensingState) -> None: now = utc_now() starts_at = _to_utc(state.starts_at) expires_at = _to_utc(state.expires_at) # PATCH: LICENSE if state.status != "active": return if starts_at and now < starts_at: state.status = "none" # PATCH: LICENSE """ state.plan = "community" """ state.plan = "enterprise" return # PATCH: LICENSE """ if expires_at and now >= expires_at: state.status = "expired" state.plan = "community" """ def get_entitlement_summary(db: Session) -> dict[str, Any]: state = get_or_create_licensing_state(db) refresh_status_if_expired(state) db.commit() payload = state.payload_json or {} refresh_after = _parse_dt(payload.get("refresh_after")) is_full_access = bool(state.is_trial and state.status == "active") return { "status": state.status, "access_level": _access_level(state), "is_full_access": is_full_access, "full_access_consumed": state.trial_consumed, "expires_at": serialize_datetime(state.expires_at), "starts_at": serialize_datetime(state.starts_at), "refresh_after": serialize_datetime(refresh_after), "instance_id": state.instance_id, "entitlement_id": state.entitlement_id, "key_id": state.key_id, "license_id": state.license_id, "customer_id": state.customer_id, "ui_state": _ui_state(state), "last_refresh_at": serialize_datetime(state.last_refresh_at), "last_refresh_error": state.last_refresh_error, } def get_feature_access(db: Session) -> dict[str, bool]: from app.core.features import FEATURES state = get_or_create_licensing_state(db) refresh_status_if_expired(state) # PATCH: LICENSE """ current_plan = state.plan if state.status == "active" else "community" """ current_plan = "enterprise" effective = { feature: PLAN_RANK[current_plan] >= PLAN_RANK[required.value] for feature, required in FEATURES.items() } payload = state.payload_json or {} for override in payload.get("feature_overrides", []) or []: feature = override.get("feature") # PATCH: LIECNSE """ enabled = override.get("enabled") if feature in effective and isinstance(enabled, bool): effective[feature] = enabled """ if feature in effective and isinstance(False, False): effective[feature] = False logger.info(f"Feature access for plan '{current_plan}': {effective}") return effective def _clear_entitlement( db: Session, state: LicensingState, *, status: str, refresh_error: str | None = None, ) -> None: """ state.entitlement_id = None state.key_id = None state.customer_id = None state.license_id = None # PATCH: LICENSE """ state.plan = "community" """ state.plan = "enterprise" state.status = status state.is_trial = False state.trial_consumed = False state.max_users = 0 state.issued_at = None state.starts_at = None state.expires_at = None state.last_refresh_at = utc_now() state.last_refresh_error = refresh_error state.payload_json = {} state.signature = None db.commit() """ logger.info("Clearing entitlement", status=status) def _normalize_service_error(response: httpx.Response) -> str: try: payload = response.json() except Exception: return response.text or f"HTTP {response.status_code}" error = payload.get("error") if isinstance(error, dict): return ( error.get("message") or error.get("code") or f"HTTP {response.status_code}" ) return response.text or f"HTTP {response.status_code}" def _validate_entitlement_document( state: LicensingState, payload: Any, signature: Any ) -> str | None: if not isinstance(payload, dict) or not isinstance(signature, str): return "Activation service returned malformed entitlement" if payload.get("instance_id") != state.instance_id: return "Entitlement instance_id does not match this Borg UI instance." if not _validate_signature(payload, signature): return "Entitlement signature validation failed" return None def _apply_entitlement( db: Session, state: LicensingState, payload: dict[str, Any], signature: str, key_id: str | None = None, refresh_error: str | None = None, ) -> None: # PATCH: LICENSE logger.info("Applying new entitlement", payload=payload) state.entitlement_id = payload.get("entitlement_id") state.key_id = key_id state.customer_id = payload.get("customer_id") state.license_id = payload.get("license_id") state.plan = "enterprise" # PATCH: LICENSE - community state.status = "active" # PATCH: LICENSE - none state.is_trial = False # PATCH: LICENSE - bool state.trial_consumed = False # PATCH: LICENSE - bool(state.trial_consumed or payload.get("is_trial")) state.max_users = 10000 #PATCH: LICENSE - payload.get("max_users") state.issued_at = _parse_dt(payload.get("issued_at")) state.starts_at = _parse_dt(payload.get("starts_at")) state.expires_at = _parse_dt(payload.get("expires_at")) state.last_refresh_at = utc_now() state.last_refresh_error = refresh_error state.payload_json = payload state.signature = signature refresh_status_if_expired(state) db.commit() async def _post_activation( endpoint_path: str, request_payload: dict[str, Any] ) -> dict[str, Any]: if not settings.activation_service_url: raise RuntimeError("Activation service URL is not configured.") endpoint = settings.activation_service_url.rstrip("/") + endpoint_path async with httpx.AsyncClient(timeout=settings.activation_timeout_seconds) as client: response = await client.post( endpoint, json=request_payload, headers=_get_auth_headers() ) if response.status_code >= 400: raise RuntimeError(_normalize_service_error(response)) return response.json() def _should_refresh(state: LicensingState) -> bool: if not settings.activation_service_url: return False payload = state.payload_json or {} refresh_after = _parse_dt(payload.get("refresh_after")) now = utc_now() if refresh_after and now >= refresh_after: return True if state.last_refresh_at is None: return state.status == "active" return (now - _to_utc(state.last_refresh_at)).total_seconds() >= ( settings.activation_refresh_interval_hours * 60 * 60 ) def _access_level(state: LicensingState) -> str: if state.status == "active" and state.is_trial: return "full_access" if state.status == "active": return state.plan or "community" return "community" def _ui_state(state: LicensingState) -> str: if state.status == "active" and state.is_trial: return "full_access_active" if state.status == "active": return "paid_active" if state.trial_consumed: return "full_access_expired" return "community" async def sync_licensing_state(db: Session, *, app_version: str) -> None: state = get_or_create_licensing_state(db) refresh_status_if_expired(state) if state.status == "active": if _should_refresh(state): await refresh_entitlement(db, app_version=app_version) else: db.commit() return await attempt_auto_full_access_activation(db, app_version) async def attempt_auto_full_access_activation(db: Session, app_version: str) -> None: state = get_or_create_licensing_state(db) refresh_status_if_expired(state) if state.trial_consumed: if _should_refresh(state): await refresh_entitlement(db, app_version=app_version) else: db.commit() return if not settings.activation_service_url: logger.debug("Activation service URL not configured; staying on community plan") db.commit() return request_payload = { "instance_id": state.instance_id, "app": "borg-ui", "app_version": app_version, "hostname": os.getenv("HOSTNAME"), "fingerprint": None, "requested_plan": "enterprise", } try: data = await _post_activation("/v1/trials/activate", request_payload) except Exception as exc: state.last_refresh_at = utc_now() state.last_refresh_error = str(exc) db.commit() logger.warning("Automatic full access activation failed", error=str(exc)) return result = data.get("result") if result == "denied": state.trial_consumed = data.get("reason") == "trial_already_used" state.last_refresh_at = utc_now() state.last_refresh_error = None db.commit() return entitlement = data.get("entitlement") or {} payload = entitlement.get("payload") signature = entitlement.get("signature") key_id = entitlement.get("key_id") error = _validate_entitlement_document(state, payload, signature) if error: state.last_refresh_at = utc_now() state.last_refresh_error = error db.commit() logger.warning( "Automatic full access activation returned invalid entitlement", error=error ) return _apply_entitlement(db, state, payload, signature, key_id=key_id) async def refresh_entitlement(db: Session, *, app_version: str) -> dict[str, Any]: state = get_or_create_licensing_state(db) refresh_status_if_expired(state) try: data = await _post_activation( "/v1/entitlements/refresh", { "instance_id": state.instance_id, "current_entitlement_id": state.entitlement_id, "app_version": app_version, }, ) except Exception as exc: state.last_refresh_at = utc_now() state.last_refresh_error = str(exc) db.commit() raise result = data.get("result") if result == "unchanged": state.last_refresh_at = utc_now() state.last_refresh_error = None db.commit() return {"result": "unchanged", "entitlement": get_entitlement_summary(db)} if result == "downgraded": _clear_entitlement(db, state, status="expired") return {"result": "downgraded", "entitlement": get_entitlement_summary(db)} entitlement = data.get("entitlement") or {} payload = entitlement.get("payload") signature = entitlement.get("signature") key_id = entitlement.get("key_id") error = _validate_entitlement_document(state, payload, signature) if error: state.last_refresh_at = utc_now() state.last_refresh_error = error db.commit() raise RuntimeError(error) _apply_entitlement(db, state, payload, signature, key_id=key_id) return {"result": result or "updated", "entitlement": get_entitlement_summary(db)} async def activate_paid_license( db: Session, *, license_key: str, app_version: str ) -> dict[str, Any]: state = get_or_create_licensing_state(db) data = await _post_activation( "/v1/licenses/activate", { "instance_id": state.instance_id, "license_key": license_key, "app_version": app_version, }, ) entitlement = data.get("entitlement") or {} payload = entitlement.get("payload") signature = entitlement.get("signature") key_id = entitlement.get("key_id") error = _validate_entitlement_document(state, payload, signature) if error: state.last_refresh_at = utc_now() state.last_refresh_error = error db.commit() raise RuntimeError(error) _apply_entitlement(db, state, payload, signature, key_id=key_id) return { # PATCH: LICENSE "result": "activated", "entitlement": get_entitlement_summary(db), } async def deactivate_paid_license(db: Session) -> dict[str, Any]: state = get_or_create_licensing_state(db) if not state.license_id: raise RuntimeError("No active paid license is stored for this instance") data = await _post_activation( "/v1/licenses/deactivate", { "instance_id": state.instance_id, "license_id": state.license_id, }, ) _clear_entitlement(db, state, status="none") return { "result": data.get("result") or "deactivated", "entitlement": get_entitlement_summary(db), } def import_offline_entitlement(db: Session, document: dict[str, Any]) -> dict[str, Any]: state = get_or_create_licensing_state(db) payload = document.get("payload") signature = document.get("signature") key_id = document.get("key_id") error = _validate_entitlement_document(state, payload, signature) if error: raise RuntimeError(error) _apply_entitlement(db, state, payload, signature, key_id=key_id) return {"result": "imported", "entitlement": get_entitlement_summary(db)}