From 0ed39d81e96dc3b5b46a989ef83dd34bb66088f9 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 4 Mar 2026 20:10:42 -0800 Subject: [PATCH] feat: add global license check middleware to block API access on expiry Add before_request middleware that validates enterprise license status for all /console/api endpoints when ENTERPRISE_ENABLED is true. Behavior: - Checks license status before each console API request - Returns 403 with clear error message when license is expired/inactive/lost - Exempts auth endpoints (login, oauth, forgot-password, etc.) - Exempts /console/api/features so frontend can fetch license status - Gracefully handles errors to avoid service disruption This ensures all business APIs are blocked when license expires, addressing the issue where APIs remained callable after expiry. --- api/app_factory.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/api/app_factory.py b/api/app_factory.py index 11568f139f..c846e56f2f 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -1,6 +1,7 @@ import logging import time +from flask import jsonify, request from opentelemetry.trace import get_current_span from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID @@ -8,6 +9,7 @@ from configs import dify_config from contexts.wrapper import RecyclableContextVar from core.logging.context import init_request_context from dify_app import DifyApp +from services.feature_service import FeatureService, LicenseStatus logger = logging.getLogger(__name__) @@ -31,6 +33,47 @@ def create_flask_app_with_configs() -> DifyApp: init_request_context() RecyclableContextVar.increment_thread_recycles() + # Enterprise license validation for console API endpoints + if dify_config.ENTERPRISE_ENABLED and request.path.startswith("/console/api"): + # Skip license check for auth-related endpoints and system endpoints + exempt_paths = [ + "/console/api/login", + "/console/api/logout", + "/console/api/oauth", + "/console/api/setup", + "/console/api/init", + "/console/api/forgot-password", + "/console/api/email-code-login", + "/console/api/activation", + "/console/api/data-source-oauth", + "/console/api/features", # Allow fetching features to show license status + ] + + # Check if current path is exempt + is_exempt = any(request.path.startswith(path) for path in exempt_paths) + + if not is_exempt: + try: + # Check license status + system_features = FeatureService.get_system_features(is_authenticated=True) + if system_features.license.status in [ + LicenseStatus.INACTIVE, + LicenseStatus.EXPIRED, + LicenseStatus.LOST, + ]: + return jsonify({ + "code": "license_expired", + "message": ( + f"Enterprise license is {system_features.license.status.value}. " + "Please contact your administrator." + ), + "status": system_features.license.status.value, + }), 403 + except Exception: + # If license check fails, log but don't block the request + # This prevents service disruption if enterprise API is temporarily unavailable + logger.exception("Failed to check enterprise license status") + # add after request hook for injecting trace headers from OpenTelemetry span context # Only adds headers when OTEL is enabled and has valid context @dify_app.after_request