From 56077cd155fd4bde3c19a0fd1b78106bbd999fb1 Mon Sep 17 00:00:00 2001 From: zhangx1n Date: Mon, 19 May 2025 22:41:44 +0800 Subject: [PATCH 1/5] feat: add plugin installation permission model and integrate into feature service --- api/services/feature_service.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index c2226c319f..369ddf232a 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -41,6 +41,18 @@ class LicenseModel(BaseModel): expired_at: str = "" +class PluginInstallationPermissionModel(BaseModel): + # Plugin installation scope – possible values: + # PLUGIN_INSTALLATION_SCOPE_NONE: prohibit all plugin installations + # PLUGIN_INSTALLATION_SCOPE_OFFICIAL_ONLY: allow only Dify official plugins + # PLUGIN_INSTALLATION_SCOPE_OFFICIAL_AND_SPECIFIC_PARTNERS: allow official and specific partner plugins + # PLUGIN_INSTALLATION_SCOPE_ALL: allow installation of all plugins + plugin_installation_scope: str = "PLUGIN_INSTALLATION_SCOPE_ALL" + + # If True, restrict plugin installation to the marketplace only + restrict_to_marketplace_only: bool = False + + class FeatureModel(BaseModel): billing: BillingModel = BillingModel() education: EducationModel = EducationModel() @@ -80,6 +92,7 @@ class SystemFeatureModel(BaseModel): is_allow_create_workspace: bool = False is_email_setup: bool = False license: LicenseModel = LicenseModel() + plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() class FeatureService: @@ -213,3 +226,12 @@ class FeatureService: if "expired_at" in license_info: features.license.expired_at = license_info["expired_at"] + + if "plugin_installation_permission" in enterprise_info: + plugin_installation_info = enterprise_info["plugin_installation_permission"] + features.plugin_installation_permission.plugin_installation_scope = plugin_installation_info[ + "plugin_installation_scope" + ] + features.plugin_installation_permission.restrict_to_marketplace_only = plugin_installation_info[ + "restrict_to_marketplace_only" + ] From f382d4eb39ab1a90e2cda1d9c57b94844f413b83 Mon Sep 17 00:00:00 2001 From: zhangx1n Date: Tue, 20 May 2025 10:48:01 +0800 Subject: [PATCH 2/5] refactor: update plugin installation scope comments and default value --- api/services/feature_service.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 369ddf232a..a0cb7372fb 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -43,11 +43,11 @@ class LicenseModel(BaseModel): class PluginInstallationPermissionModel(BaseModel): # Plugin installation scope – possible values: - # PLUGIN_INSTALLATION_SCOPE_NONE: prohibit all plugin installations - # PLUGIN_INSTALLATION_SCOPE_OFFICIAL_ONLY: allow only Dify official plugins - # PLUGIN_INSTALLATION_SCOPE_OFFICIAL_AND_SPECIFIC_PARTNERS: allow official and specific partner plugins - # PLUGIN_INSTALLATION_SCOPE_ALL: allow installation of all plugins - plugin_installation_scope: str = "PLUGIN_INSTALLATION_SCOPE_ALL" + # none: prohibit all plugin installations + # official_only: allow only Dify official plugins + # official_and_specific_partners: allow official and specific partner plugins + # all: allow installation of all plugins + plugin_installation_scope: str = "all" # If True, restrict plugin installation to the marketplace only restrict_to_marketplace_only: bool = False From 403544f1d4724bab2e5507722cf524492507a8d3 Mon Sep 17 00:00:00 2001 From: zhangx1n Date: Tue, 20 May 2025 12:16:45 +0800 Subject: [PATCH 3/5] refactor --- api/services/feature_service.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/services/feature_service.py b/api/services/feature_service.py index be85a03e80..8fd61c221a 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -88,6 +88,18 @@ class WebAppAuthModel(BaseModel): allow_email_password_login: bool = False +class PluginInstallationPermissionModel(BaseModel): + # Plugin installation scope – possible values: + # none: prohibit all plugin installations + # official_only: allow only Dify official plugins + # official_and_specific_partners: allow official and specific partner plugins + # all: allow installation of all plugins + plugin_installation_scope: str = "all" + + # If True, restrict plugin installation to the marketplace only + restrict_to_marketplace_only: bool = False + + class FeatureModel(BaseModel): billing: BillingModel = BillingModel() education: EducationModel = EducationModel() @@ -128,6 +140,7 @@ class SystemFeatureModel(BaseModel): license: LicenseModel = LicenseModel() branding: BrandingModel = BrandingModel() webapp_auth: WebAppAuthModel = WebAppAuthModel() + plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() class FeatureService: @@ -291,3 +304,12 @@ class FeatureService: features.license.workspaces.enabled = license_info["workspaces"]["enabled"] features.license.workspaces.limit = license_info["workspaces"]["limit"] features.license.workspaces.size = license_info["workspaces"]["used"] + + if "PluginInstallationPermission" in enterprise_info: + plugin_installation_info = enterprise_info["PluginInstallationPermission"] + features.plugin_installation_permission.plugin_installation_scope = plugin_installation_info[ + "pluginInstallationScope" + ] + features.plugin_installation_permission.restrict_to_marketplace_only = plugin_installation_info[ + "restrictToMarketplaceOnly" + ] From 153abb181d2ed9175f0339f6d340dab89531503b Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Tue, 20 May 2025 14:53:23 +0800 Subject: [PATCH 4/5] feat: add PluginVerification --- api/core/plugin/entities/plugin_daemon.py | 14 ++++++++++++++ api/services/feature_service.py | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index 2bea07bea0..b03e5ed1a6 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -156,9 +156,23 @@ class PluginInstallTaskStartResponse(BaseModel): task_id: str = Field(description="The ID of the install task.") +class PluginVerification(BaseModel): + """ + Verification of the plugin. + """ + + class AuthorizedCategory(StrEnum): + Langgenius = "langgenius" + Partner = "partner" + Community = "community" + + authorized_category: AuthorizedCategory = Field(description="The authorized category of the plugin.") + + class PluginUploadResponse(BaseModel): unique_identifier: str = Field(description="The unique identifier of the plugin.") manifest: PluginDeclaration + verification: Optional[PluginVerification] = Field(default=None, description="Basic verification information") class PluginOAuthAuthorizationUrlResponse(BaseModel): diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 8fd61c221a..312e3acc3f 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -88,13 +88,20 @@ class WebAppAuthModel(BaseModel): allow_email_password_login: bool = False +class PluginInstallationScope(StrEnum): + NONE = "none" + OFFICIAL_ONLY = "official_only" + OFFICIAL_AND_SPECIFIC_PARTNERS = "official_and_specific_partners" + ALL = "all" + + class PluginInstallationPermissionModel(BaseModel): # Plugin installation scope – possible values: # none: prohibit all plugin installations # official_only: allow only Dify official plugins # official_and_specific_partners: allow official and specific partner plugins # all: allow installation of all plugins - plugin_installation_scope: str = "all" + plugin_installation_scope: PluginInstallationScope = PluginInstallationScope.ALL # If True, restrict plugin installation to the marketplace only restrict_to_marketplace_only: bool = False From a6c98d71dbf5d40debef534953162ae2f9709f8c Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Tue, 20 May 2025 15:23:58 +0800 Subject: [PATCH 5/5] feat: implement plugin installation availability checks and add custom error handling --- api/services/errors/plugin.py | 5 ++ api/services/feature_service.py | 1 + api/services/plugin/plugin_service.py | 91 +++++++++++++++++++++++---- 3 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 api/services/errors/plugin.py diff --git a/api/services/errors/plugin.py b/api/services/errors/plugin.py new file mode 100644 index 0000000000..be5b144b3d --- /dev/null +++ b/api/services/errors/plugin.py @@ -0,0 +1,5 @@ +from services.errors.base import BaseServiceError + + +class PluginInstallationForbiddenError(BaseServiceError): + pass diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 312e3acc3f..188caf3505 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -104,6 +104,7 @@ class PluginInstallationPermissionModel(BaseModel): plugin_installation_scope: PluginInstallationScope = PluginInstallationScope.ALL # If True, restrict plugin installation to the marketplace only + # Equivalent to ForceEnablePluginVerification restrict_to_marketplace_only: bool = False diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index be722a59ad..124154da59 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -17,11 +17,13 @@ from core.plugin.entities.plugin import ( PluginInstallation, PluginInstallationSource, ) -from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginUploadResponse +from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginUploadResponse, PluginVerification from core.plugin.impl.asset import PluginAssetManager from core.plugin.impl.debugging import PluginDebuggingClient from core.plugin.impl.plugin import PluginInstaller from extensions.ext_redis import redis_client +from services.errors.plugin import PluginInstallationForbiddenError +from services.feature_service import FeatureService, PluginInstallationScope logger = logging.getLogger(__name__) @@ -86,6 +88,35 @@ class PluginService: logger.exception("failed to fetch latest plugin version") return result + @staticmethod + def _check_plugin_installation_availability(plugin_verification: Optional[PluginVerification]): + """ + Check the verification of the plugin + """ + features = FeatureService.get_system_features() + + if not plugin_verification: + if features.plugin_installation_permission.restrict_to_marketplace_only: + raise PluginInstallationForbiddenError("Plugin installation is restricted to marketplace only") + return + + match features.plugin_installation_permission.plugin_installation_scope: + case PluginInstallationScope.OFFICIAL_ONLY: + if plugin_verification.authorized_category != PluginVerification.AuthorizedCategory.Langgenius: + raise PluginInstallationForbiddenError("Plugin installation is restricted to official only") + case PluginInstallationScope.OFFICIAL_AND_SPECIFIC_PARTNERS: + if plugin_verification.authorized_category not in [ + PluginVerification.AuthorizedCategory.Langgenius, + PluginVerification.AuthorizedCategory.Partner, + ]: + raise PluginInstallationForbiddenError( + "Plugin installation is restricted to official and specific partners" + ) + case PluginInstallationScope.NONE: + raise PluginInstallationForbiddenError("Installing plugins is not allowed") + case PluginInstallationScope.ALL: + pass + @staticmethod def get_debugging_key(tenant_id: str) -> str: """ @@ -199,6 +230,8 @@ class PluginService: # check if plugin pkg is already downloaded manager = PluginInstaller() + features = FeatureService.get_system_features() + try: manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier) # already downloaded, skip, and record install event @@ -206,7 +239,14 @@ class PluginService: except Exception: # plugin not installed, download and upload pkg pkg = download_plugin_pkg(new_plugin_unique_identifier) - manager.upload_pkg(tenant_id, pkg, verify_signature=False) + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + + # check if the plugin is available to install + PluginService._check_plugin_installation_availability(response.verification) return manager.upgrade_plugin( tenant_id, @@ -251,7 +291,15 @@ class PluginService: returns: plugin_unique_identifier """ manager = PluginInstaller() - return manager.upload_pkg(tenant_id, pkg, verify_signature) + features = FeatureService.get_system_features() + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + # check if the plugin is available to install + PluginService._check_plugin_installation_availability(response.verification) + return response @staticmethod def upload_pkg_from_github( @@ -264,13 +312,17 @@ class PluginService: pkg = download_with_size_limit( f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE ) + features = FeatureService.get_system_features() manager = PluginInstaller() - return manager.upload_pkg( + response = manager.upload_pkg( tenant_id, pkg, - verify_signature, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, ) + # check if the plugin is available to install + PluginService._check_plugin_installation_availability(response.verification) + return response @staticmethod def upload_bundle( @@ -313,28 +365,33 @@ class PluginService: ) @staticmethod - def fetch_marketplace_pkg( - tenant_id: str, plugin_unique_identifier: str, verify_signature: bool = False - ) -> PluginDeclaration: + def fetch_marketplace_pkg(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration: """ Fetch marketplace package """ if not dify_config.MARKETPLACE_ENABLED: raise ValueError("marketplace is not enabled") + features = FeatureService.get_system_features() + manager = PluginInstaller() try: declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) except Exception: pkg = download_plugin_pkg(plugin_unique_identifier) - declaration = manager.upload_pkg(tenant_id, pkg, verify_signature).manifest + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + # check if the plugin is available to install + PluginService._check_plugin_installation_availability(response.verification) + declaration = response.manifest return declaration @staticmethod - def install_from_marketplace_pkg( - tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False - ): + def install_from_marketplace_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]): """ Install plugin from marketplace package files, returns installation task id @@ -344,6 +401,8 @@ class PluginService: manager = PluginInstaller() + features = FeatureService.get_system_features() + # check if already downloaded for plugin_unique_identifier in plugin_unique_identifiers: try: @@ -352,7 +411,13 @@ class PluginService: except Exception: # plugin not installed, download and upload pkg pkg = download_plugin_pkg(plugin_unique_identifier) - manager.upload_pkg(tenant_id, pkg, verify_signature) + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + # check if the plugin is available to install + PluginService._check_plugin_installation_availability(response.verification) return manager.install_from_identifiers( tenant_id,