Compare commits

..

1 Commits

Author SHA1 Message Date
GareArc
9a682f1009 fix: use LicenseStatus enum instead of raw strings and tighten path prefix matching
Replace raw license status strings with LicenseStatus enum values in
app_factory.py and enterprise_service.py to prevent silent mismatches.
Use trailing-slash prefixes ('/console/api/', '/api/') to avoid false
matches on unrelated paths like /api-docs.
2026-03-05 01:16:45 -08:00
3 changed files with 31 additions and 12 deletions

View File

@@ -11,6 +11,7 @@ from controllers.console.error import UnauthorizedAndForceLogout
from core.logging.context import init_request_context
from dify_app import DifyApp
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import LicenseStatus
logger = logging.getLogger(__name__)
@@ -38,14 +39,27 @@ def create_flask_app_with_configs() -> DifyApp:
# When license expires, block all API access except bootstrap endpoints needed
# for the frontend to load the license expiration page without infinite reloads.
if dify_config.ENTERPRISE_ENABLED:
is_console_api = request.path.startswith("/console/api")
is_webapp_api = request.path.startswith("/api") and not is_console_api
is_console_api = request.path.startswith("/console/api/")
is_webapp_api = request.path.startswith("/api/") and not is_console_api
if is_console_api or is_webapp_api:
if is_console_api:
# Console bootstrap APIs exempt from license check:
# - system-features: license status for expiry UI (GlobalPublicStoreProvider)
# - setup: install/setup status check (AppInitializer)
# - features: billing/plan features (ProviderContextProvider)
# - account/profile: login check + user profile (AppContextProvider, useIsLogin)
# - workspaces/current: workspace + model providers (AppContextProvider)
# - version: version check (AppContextProvider)
# - activate/check: invitation link validation (signin page)
# Without these exemptions, the signin page triggers location.reload()
# on unauthorized_and_force_logout, causing an infinite loop.
console_exempt_prefixes = (
"/console/api/system-features",
"/console/api/setup",
"/console/api/features",
"/console/api/account/profile",
"/console/api/workspaces/current",
"/console/api/version",
"/console/api/activate/check",
)
@@ -57,14 +71,20 @@ def create_flask_app_with_configs() -> DifyApp:
try:
# Check license status with caching (10 min TTL)
license_status = EnterpriseService.get_cached_license_status()
if license_status in ["inactive", "expired", "lost"]:
if license_status in (LicenseStatus.INACTIVE, LicenseStatus.EXPIRED, LicenseStatus.LOST):
# Cookie clearing is handled by register_external_error_handlers
# in libs/external_api.py which detects the error code and calls
# build_force_logout_cookie_headers(). Frontend then checks
# code === 'unauthorized_and_force_logout' and calls location.reload().
raise UnauthorizedAndForceLogout(
f"Enterprise license is {license_status}. "
"Please contact your administrator."
f"Enterprise license is {license_status}. Please contact your administrator."
)
except UnauthorizedAndForceLogout:
raise
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

View File

@@ -101,10 +101,7 @@ class BaseRequest:
# {"message": "..."}
# {"detail": "..."}
error_message = (
error_data.get("message")
or error_data.get("error")
or error_data.get("detail")
or error_message
error_data.get("message") or error_data.get("error") or error_data.get("detail") or error_message
)
except Exception:
# If JSON parsing fails, use the default message

View File

@@ -258,9 +258,11 @@ class EnterpriseService:
info = cls.get_info()
license_info = info.get("License")
if license_info:
status = license_info.get("status", "inactive")
from services.feature_service import LicenseStatus
status = license_info.get("status", LicenseStatus.INACTIVE)
# Only cache valid statuses so license updates are picked up immediately
if status in ("active", "expiring"):
if status in (LicenseStatus.ACTIVE, LicenseStatus.EXPIRING):
try:
redis_client.setex(LICENSE_STATUS_CACHE_KEY, LICENSE_STATUS_CACHE_TTL, status)
except Exception:
@@ -269,4 +271,4 @@ class EnterpriseService:
except Exception:
logger.exception("Failed to get enterprise license status")
return None
return None