mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
288 lines
14 KiB
Python
288 lines
14 KiB
Python
"""Deployment-wide feature policies and the public system-features snapshot."""
|
|
|
|
import logging
|
|
from collections.abc import Mapping
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
|
|
from configs import dify_config
|
|
from enums import DeploymentEdition
|
|
from services.enterprise.enterprise_service import EnterpriseService
|
|
from services.entities import feature_entities
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class _EnterprisePluginInstallationPermission(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
plugin_installation_scope: feature_entities.PluginInstallationScope = Field(alias="pluginInstallationScope")
|
|
restrict_to_marketplace_only: bool = Field(alias="restrictToMarketplaceOnly", strict=True)
|
|
|
|
|
|
class SystemFeatureService:
|
|
"""Resolve deployment-wide policies without exposing the public response DTO internally."""
|
|
|
|
@classmethod
|
|
def get_public_system_features(cls) -> feature_entities.SystemFeatureModel:
|
|
"""Build the non-sensitive bootstrap snapshot shared by Console and Web."""
|
|
system_features = feature_entities.SystemFeatureModel(deployment_edition=dify_config.DEPLOYMENT_EDITION)
|
|
system_features.rbac_enabled = dify_config.RBAC_ENABLED
|
|
|
|
cls._fulfill_system_params_from_env(system_features)
|
|
|
|
if cls.is_webapp_auth_enabled():
|
|
system_features.branding.enabled = True
|
|
system_features.webapp_auth.enabled = True
|
|
system_features.enable_change_email = False
|
|
cls._fulfill_params_from_enterprise(system_features)
|
|
|
|
if dify_config.MARKETPLACE_ENABLED:
|
|
system_features.enable_marketplace = True
|
|
|
|
if dify_config.CREATORS_PLATFORM_FEATURES_ENABLED:
|
|
system_features.enable_creators_platform = True
|
|
|
|
return system_features
|
|
|
|
@classmethod
|
|
def is_registration_allowed(cls) -> bool:
|
|
"""Return the effective registration policy, including the Enterprise override."""
|
|
is_allowed = dify_config.ALLOW_REGISTER
|
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
|
return is_allowed
|
|
|
|
enterprise_info = EnterpriseService.get_info()
|
|
return bool(enterprise_info.get("IsAllowRegister", is_allowed))
|
|
|
|
@classmethod
|
|
def is_email_password_login_enabled(cls) -> bool:
|
|
"""Return the effective password-login policy, including the Enterprise override."""
|
|
is_enabled = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN
|
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
|
return is_enabled
|
|
|
|
enterprise_info = EnterpriseService.get_info()
|
|
return bool(enterprise_info.get("EnableEmailPasswordLogin", is_enabled))
|
|
|
|
@staticmethod
|
|
def is_change_email_enabled() -> bool:
|
|
"""Return whether Console accounts may change their email address."""
|
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
|
|
return False
|
|
return dify_config.ENABLE_CHANGE_EMAIL
|
|
|
|
@staticmethod
|
|
def is_webapp_auth_enabled(*, deployment_edition: DeploymentEdition | None = None) -> bool:
|
|
"""Return whether deployment-level WebApp authentication integration is enabled."""
|
|
edition = deployment_edition if deployment_edition is not None else dify_config.DEPLOYMENT_EDITION
|
|
return edition == DeploymentEdition.ENTERPRISE
|
|
|
|
@classmethod
|
|
def get_license_status(cls) -> feature_entities.LicenseStatus:
|
|
"""Return the deployment license status used by internal admission policies."""
|
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
|
return feature_entities.LicenseStatus.NONE
|
|
return cls._resolve_license_status(EnterpriseService.get_info())
|
|
|
|
@classmethod
|
|
def get_branding(cls) -> feature_entities.BrandingModel:
|
|
"""Return the deployment branding used by server-rendered email."""
|
|
branding = feature_entities.BrandingModel(enabled=cls.is_webapp_auth_enabled())
|
|
if not branding.enabled:
|
|
return branding
|
|
|
|
enterprise_info = EnterpriseService.get_info()
|
|
if branding_info := enterprise_info.get("Branding"):
|
|
branding.application_title = branding_info.get("applicationTitle", "")
|
|
branding.login_page_logo = branding_info.get("loginPageLogo", "")
|
|
branding.workspace_logo = branding_info.get("workspaceLogo", "")
|
|
branding.favicon = branding_info.get("favicon", "")
|
|
return branding
|
|
|
|
@classmethod
|
|
def is_workspace_creation_allowed(cls) -> bool:
|
|
"""Resolve the backend workspace-creation policy, including the Enterprise override."""
|
|
is_allowed = dify_config.ALLOW_CREATE_WORKSPACE
|
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
|
return is_allowed
|
|
|
|
enterprise_info = EnterpriseService.get_info()
|
|
return bool(enterprise_info.get("IsAllowCreateWorkspace", is_allowed))
|
|
|
|
@staticmethod
|
|
def is_plugin_manager_enabled() -> bool:
|
|
"""Return whether Enterprise plugin credential policies must be enforced."""
|
|
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
|
|
|
|
@classmethod
|
|
def get_plugin_installation_permission(cls) -> feature_entities.PluginInstallationPermissionModel:
|
|
"""Resolve the validated deployment-wide plugin installation policy."""
|
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
|
return feature_entities.PluginInstallationPermissionModel()
|
|
|
|
return cls._resolve_plugin_installation_permission(EnterpriseService.get_info())
|
|
|
|
@classmethod
|
|
def get_license(cls) -> feature_entities.LicenseModel:
|
|
"""Return full license detail for authenticated server-side consumers."""
|
|
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.ENTERPRISE:
|
|
return feature_entities.LicenseModel()
|
|
license_model = cls._build_license(EnterpriseService.get_info())
|
|
license_model.license_expiry_notice_enabled = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
|
|
return license_model
|
|
|
|
@staticmethod
|
|
def is_explore_banner_enabled() -> bool:
|
|
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ENABLE_EXPLORE_BANNER
|
|
|
|
@staticmethod
|
|
def is_trial_app_enabled() -> bool:
|
|
return dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD and dify_config.ENABLE_TRIAL_APP
|
|
|
|
@classmethod
|
|
def _fulfill_system_params_from_env(cls, system_features: feature_entities.SystemFeatureModel) -> None:
|
|
system_features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN
|
|
system_features.enable_email_password_login = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN
|
|
system_features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN
|
|
system_features.enable_collaboration_mode = dify_config.ENABLE_COLLABORATION_MODE
|
|
system_features.is_allow_register = dify_config.ALLOW_REGISTER
|
|
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
|
|
system_features.enable_change_email = dify_config.ENABLE_CHANGE_EMAIL
|
|
system_features.enable_explore_banner = cls.is_explore_banner_enabled()
|
|
system_features.enable_learn_app = dify_config.ENABLE_LEARN_APP
|
|
system_features.webapp_auth.allow_public_access = dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED
|
|
system_features.enable_step_by_step_tour = dify_config.ENABLE_STEP_BY_STEP_TOUR
|
|
system_features.knowledge_fs_enabled = dify_config.KNOWLEDGE_FS_ENABLED
|
|
|
|
@classmethod
|
|
def _fulfill_params_from_enterprise(cls, features: feature_entities.SystemFeatureModel) -> None:
|
|
enterprise_info = EnterpriseService.get_info()
|
|
|
|
if "SSOEnforcedForSignin" in enterprise_info:
|
|
features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
|
|
|
|
features.sso_enforced_for_signin_protocol = cls._resolve_sso_protocol(
|
|
enterprise_info.get("SSOEnforcedForSigninProtocol"),
|
|
field_name="SSOEnforcedForSigninProtocol",
|
|
)
|
|
|
|
if "EnableEmailCodeLogin" in enterprise_info:
|
|
features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
|
|
|
|
if "EnableEmailPasswordLogin" in enterprise_info:
|
|
features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
|
|
|
|
if "IsAllowRegister" in enterprise_info:
|
|
features.is_allow_register = enterprise_info["IsAllowRegister"]
|
|
|
|
if "EnableAppDeploy" in enterprise_info:
|
|
features.enable_app_deploy = enterprise_info["EnableAppDeploy"]
|
|
|
|
if "Branding" in enterprise_info:
|
|
features.branding.application_title = enterprise_info["Branding"].get("applicationTitle", "")
|
|
features.branding.login_page_logo = enterprise_info["Branding"].get("loginPageLogo", "")
|
|
features.branding.workspace_logo = enterprise_info["Branding"].get("workspaceLogo", "")
|
|
features.branding.favicon = enterprise_info["Branding"].get("favicon", "")
|
|
|
|
if "WebAppAuth" in enterprise_info:
|
|
features.webapp_auth.allow_sso = enterprise_info["WebAppAuth"].get("allowSso", False)
|
|
features.webapp_auth.allow_email_code_login = enterprise_info["WebAppAuth"].get(
|
|
"allowEmailCodeLogin", False
|
|
)
|
|
features.webapp_auth.allow_email_password_login = enterprise_info["WebAppAuth"].get(
|
|
"allowEmailPasswordLogin", False
|
|
)
|
|
features.webapp_auth.sso_config.protocol = cls._resolve_sso_protocol(
|
|
enterprise_info.get("SSOEnforcedForWebProtocol"),
|
|
field_name="SSOEnforcedForWebProtocol",
|
|
)
|
|
|
|
# The unauthenticated endpoint exposes status only. Full license detail is
|
|
# served by the authenticated license endpoint.
|
|
license_status = cls._resolve_license_status(enterprise_info)
|
|
if license_status != feature_entities.LicenseStatus.NONE:
|
|
features.license = feature_entities.LicenseStatusModel(
|
|
status=license_status,
|
|
)
|
|
|
|
features.plugin_installation_permission = cls._resolve_plugin_installation_permission(enterprise_info)
|
|
|
|
@staticmethod
|
|
def _resolve_license_status(enterprise_info: Mapping[str, object]) -> feature_entities.LicenseStatus:
|
|
license_info = enterprise_info.get("License")
|
|
if not license_info:
|
|
return feature_entities.LicenseStatus.NONE
|
|
if not isinstance(license_info, Mapping):
|
|
return feature_entities.LicenseStatus.INACTIVE
|
|
|
|
status = license_info.get("status", feature_entities.LicenseStatus.INACTIVE)
|
|
if isinstance(status, feature_entities.LicenseStatus):
|
|
return status
|
|
if isinstance(status, str):
|
|
return feature_entities.LicenseStatus(status)
|
|
return feature_entities.LicenseStatus.INACTIVE
|
|
|
|
@classmethod
|
|
def _build_license(cls, enterprise_info: dict) -> feature_entities.LicenseModel:
|
|
license_model = feature_entities.LicenseModel()
|
|
if license_info := enterprise_info.get("License"):
|
|
license_model.status = feature_entities.LicenseStatus(
|
|
license_info.get("status", feature_entities.LicenseStatus.INACTIVE)
|
|
)
|
|
license_model.expired_at = license_info.get("expiredAt", "")
|
|
if workspaces_info := license_info.get("workspaces"):
|
|
license_model.workspaces = feature_entities.LicenseLimitationModel(
|
|
enabled=workspaces_info.get("enabled", False),
|
|
limit=workspaces_info.get("limit", 0),
|
|
size=workspaces_info.get("used", 0),
|
|
)
|
|
if seats_info := license_info.get("licensedSeats"):
|
|
license_model.seats = feature_entities.LicenseLimitationModel(
|
|
enabled=seats_info.get("enabled", False),
|
|
limit=seats_info.get("limit", 0),
|
|
size=seats_info.get("used", 0),
|
|
)
|
|
return license_model
|
|
|
|
@classmethod
|
|
def _resolve_plugin_installation_permission(
|
|
cls, enterprise_info: Mapping[str, object]
|
|
) -> feature_entities.PluginInstallationPermissionModel:
|
|
if "PluginInstallationPermission" not in enterprise_info:
|
|
return feature_entities.PluginInstallationPermissionModel()
|
|
|
|
try:
|
|
permission = _EnterprisePluginInstallationPermission.model_validate(
|
|
enterprise_info["PluginInstallationPermission"]
|
|
)
|
|
except ValidationError as exc:
|
|
logger.error( # noqa: TRY400
|
|
"Invalid Enterprise plugin installation permission; denying all plugin installations: %s",
|
|
exc.errors(include_input=False),
|
|
)
|
|
return feature_entities.PluginInstallationPermissionModel(
|
|
plugin_installation_scope=feature_entities.PluginInstallationScope.NONE,
|
|
restrict_to_marketplace_only=True,
|
|
)
|
|
|
|
return feature_entities.PluginInstallationPermissionModel(
|
|
plugin_installation_scope=permission.plugin_installation_scope,
|
|
restrict_to_marketplace_only=permission.restrict_to_marketplace_only,
|
|
)
|
|
|
|
@staticmethod
|
|
def _resolve_sso_protocol(value: object, *, field_name: str) -> feature_entities.SSOProtocol | None:
|
|
if value is None or (isinstance(value, str) and not value.strip()):
|
|
return None
|
|
|
|
if not isinstance(value, str):
|
|
logger.error("Invalid Enterprise SSO protocol for %s; disabling the protocol", field_name)
|
|
return None
|
|
|
|
try:
|
|
return feature_entities.SSOProtocol(value)
|
|
except ValueError:
|
|
logger.error("Invalid Enterprise SSO protocol for %s; disabling the protocol", field_name) # noqa: TRY400
|
|
return None
|