mirror of
https://github.com/langgenius/dify.git
synced 2026-03-06 15:45:14 +00:00
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.
219 lines
8.2 KiB
Python
219 lines
8.2 KiB
Python
import logging
|
|
import time
|
|
|
|
from flask import request
|
|
from opentelemetry.trace import get_current_span
|
|
from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID
|
|
|
|
from configs import dify_config
|
|
from contexts.wrapper import RecyclableContextVar
|
|
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__)
|
|
|
|
|
|
# ----------------------------
|
|
# Application Factory Function
|
|
# ----------------------------
|
|
def create_flask_app_with_configs() -> DifyApp:
|
|
"""
|
|
create a raw flask app
|
|
with configs loaded from .env file
|
|
"""
|
|
dify_app = DifyApp(__name__)
|
|
dify_app.config.from_mapping(dify_config.model_dump())
|
|
dify_app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
|
|
|
# add before request hook
|
|
@dify_app.before_request
|
|
def before_request():
|
|
# Initialize logging context for this request
|
|
init_request_context()
|
|
RecyclableContextVar.increment_thread_recycles()
|
|
|
|
# Enterprise license validation for API endpoints (both console and webapp)
|
|
# 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
|
|
|
|
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",
|
|
)
|
|
is_exempt = any(request.path.startswith(p) for p in console_exempt_prefixes)
|
|
else: # webapp API
|
|
is_exempt = request.path.startswith("/api/system-features")
|
|
|
|
if not is_exempt:
|
|
try:
|
|
# Check license status with caching (10 min TTL)
|
|
license_status = EnterpriseService.get_cached_license_status()
|
|
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."
|
|
)
|
|
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
|
|
# Only adds headers when OTEL is enabled and has valid context
|
|
@dify_app.after_request
|
|
def add_trace_headers(response):
|
|
try:
|
|
span = get_current_span()
|
|
ctx = span.get_span_context() if span else None
|
|
|
|
if not ctx or not ctx.is_valid:
|
|
return response
|
|
|
|
# Inject trace headers from OTEL context
|
|
if ctx.trace_id != INVALID_TRACE_ID and "X-Trace-Id" not in response.headers:
|
|
response.headers["X-Trace-Id"] = format(ctx.trace_id, "032x")
|
|
if ctx.span_id != INVALID_SPAN_ID and "X-Span-Id" not in response.headers:
|
|
response.headers["X-Span-Id"] = format(ctx.span_id, "016x")
|
|
|
|
except Exception:
|
|
# Never break the response due to tracing header injection
|
|
logger.warning("Failed to add trace headers to response", exc_info=True)
|
|
return response
|
|
|
|
# Capture the decorator's return value to avoid pyright reportUnusedFunction
|
|
_ = before_request
|
|
_ = add_trace_headers
|
|
|
|
return dify_app
|
|
|
|
|
|
def create_app() -> DifyApp:
|
|
start_time = time.perf_counter()
|
|
app = create_flask_app_with_configs()
|
|
initialize_extensions(app)
|
|
end_time = time.perf_counter()
|
|
if dify_config.DEBUG:
|
|
logger.info("Finished create_app (%s ms)", round((end_time - start_time) * 1000, 2))
|
|
return app
|
|
|
|
|
|
def initialize_extensions(app: DifyApp):
|
|
# Initialize Flask context capture for workflow execution
|
|
from context.flask_app_context import init_flask_context
|
|
from extensions import (
|
|
ext_app_metrics,
|
|
ext_blueprints,
|
|
ext_celery,
|
|
ext_code_based_extension,
|
|
ext_commands,
|
|
ext_compress,
|
|
ext_database,
|
|
ext_enterprise_telemetry,
|
|
ext_fastopenapi,
|
|
ext_forward_refs,
|
|
ext_hosting_provider,
|
|
ext_import_modules,
|
|
ext_logging,
|
|
ext_login,
|
|
ext_logstore,
|
|
ext_mail,
|
|
ext_migrate,
|
|
ext_orjson,
|
|
ext_otel,
|
|
ext_proxy_fix,
|
|
ext_redis,
|
|
ext_request_logging,
|
|
ext_sentry,
|
|
ext_session_factory,
|
|
ext_set_secretkey,
|
|
ext_storage,
|
|
ext_timezone,
|
|
ext_warnings,
|
|
)
|
|
|
|
init_flask_context()
|
|
|
|
extensions = [
|
|
ext_timezone,
|
|
ext_logging,
|
|
ext_warnings,
|
|
ext_import_modules,
|
|
ext_orjson,
|
|
ext_forward_refs,
|
|
ext_set_secretkey,
|
|
ext_compress,
|
|
ext_code_based_extension,
|
|
ext_database,
|
|
ext_app_metrics,
|
|
ext_migrate,
|
|
ext_redis,
|
|
ext_storage,
|
|
ext_logstore, # Initialize logstore after storage, before celery
|
|
ext_celery,
|
|
ext_login,
|
|
ext_mail,
|
|
ext_hosting_provider,
|
|
ext_sentry,
|
|
ext_proxy_fix,
|
|
ext_blueprints,
|
|
ext_commands,
|
|
ext_fastopenapi,
|
|
ext_otel,
|
|
ext_enterprise_telemetry,
|
|
ext_request_logging,
|
|
ext_session_factory,
|
|
]
|
|
for ext in extensions:
|
|
short_name = ext.__name__.split(".")[-1]
|
|
is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
|
|
if not is_enabled:
|
|
if dify_config.DEBUG:
|
|
logger.info("Skipped %s", short_name)
|
|
continue
|
|
|
|
start_time = time.perf_counter()
|
|
ext.init_app(app)
|
|
end_time = time.perf_counter()
|
|
if dify_config.DEBUG:
|
|
logger.info("Loaded %s (%s ms)", short_name, round((end_time - start_time) * 1000, 2))
|
|
|
|
|
|
def create_migrations_app() -> DifyApp:
|
|
app = create_flask_app_with_configs()
|
|
from extensions import ext_database, ext_migrate
|
|
|
|
# Initialize only required extensions
|
|
ext_database.init_app(app)
|
|
ext_migrate.init_app(app)
|
|
|
|
return app
|