Feat/plugin install scope management (#19963)

This commit is contained in:
Xin Zhang
2025-06-18 16:25:00 +08:00
committed by GitHub
parent 420a34a546
commit 30cfc9c172
6 changed files with 166 additions and 20 deletions

View File

@@ -156,9 +156,23 @@ class PluginInstallTaskStartResponse(BaseModel):
task_id: str = Field(description="The ID of the install task.") task_id: str = Field(description="The ID of the install task.")
class PluginUploadResponse(BaseModel): 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 PluginDecodeResponse(BaseModel):
unique_identifier: str = Field(description="The unique identifier of the plugin.") unique_identifier: str = Field(description="The unique identifier of the plugin.")
manifest: PluginDeclaration manifest: PluginDeclaration
verification: Optional[PluginVerification] = Field(default=None, description="Basic verification information")
class PluginOAuthAuthorizationUrlResponse(BaseModel): class PluginOAuthAuthorizationUrlResponse(BaseModel):

View File

@@ -10,10 +10,10 @@ from core.plugin.entities.plugin import (
PluginInstallationSource, PluginInstallationSource,
) )
from core.plugin.entities.plugin_daemon import ( from core.plugin.entities.plugin_daemon import (
PluginDecodeResponse,
PluginInstallTask, PluginInstallTask,
PluginInstallTaskStartResponse, PluginInstallTaskStartResponse,
PluginListResponse, PluginListResponse,
PluginUploadResponse,
) )
from core.plugin.impl.base import BasePluginClient from core.plugin.impl.base import BasePluginClient
@@ -53,7 +53,7 @@ class PluginInstaller(BasePluginClient):
tenant_id: str, tenant_id: str,
pkg: bytes, pkg: bytes,
verify_signature: bool = False, verify_signature: bool = False,
) -> PluginUploadResponse: ) -> PluginDecodeResponse:
""" """
Upload a plugin package and return the plugin unique identifier. Upload a plugin package and return the plugin unique identifier.
""" """
@@ -68,7 +68,7 @@ class PluginInstaller(BasePluginClient):
return self._request_with_plugin_daemon_response( return self._request_with_plugin_daemon_response(
"POST", "POST",
f"plugin/{tenant_id}/management/install/upload/package", f"plugin/{tenant_id}/management/install/upload/package",
PluginUploadResponse, PluginDecodeResponse,
files=body, files=body,
data=data, data=data,
) )
@@ -176,6 +176,18 @@ class PluginInstaller(BasePluginClient):
params={"plugin_unique_identifier": plugin_unique_identifier}, params={"plugin_unique_identifier": plugin_unique_identifier},
) )
def decode_plugin_from_identifier(self, tenant_id: str, plugin_unique_identifier: str) -> PluginDecodeResponse:
"""
Decode a plugin from an identifier.
"""
return self._request_with_plugin_daemon_response(
"GET",
f"plugin/{tenant_id}/management/decode/from_identifier",
PluginDecodeResponse,
data={"plugin_unique_identifier": plugin_unique_identifier},
headers={"Content-Type": "application/json"},
)
def fetch_plugin_installation_by_ids( def fetch_plugin_installation_by_ids(
self, tenant_id: str, plugin_ids: Sequence[str] self, tenant_id: str, plugin_ids: Sequence[str]
) -> Sequence[PluginInstallation]: ) -> Sequence[PluginInstallation]:

View File

@@ -10,7 +10,6 @@ from core.plugin.entities.plugin import GenericProviderID
from core.tools.entities.tool_entities import ToolProviderType from core.tools.entities.tool_entities import ToolProviderType
from core.tools.signature import sign_tool_file from core.tools.signature import sign_tool_file
from core.workflow.entities.workflow_execution import WorkflowExecutionStatus from core.workflow.entities.workflow_execution import WorkflowExecutionStatus
from services.plugin.plugin_service import PluginService
if TYPE_CHECKING: if TYPE_CHECKING:
from models.workflow import Workflow from models.workflow import Workflow
@@ -169,6 +168,7 @@ class App(Base):
@property @property
def deleted_tools(self) -> list: def deleted_tools(self) -> list:
from core.tools.tool_manager import ToolManager from core.tools.tool_manager import ToolManager
from services.plugin.plugin_service import PluginService
# get agent mode tools # get agent mode tools
app_model_config = self.app_model_config app_model_config = self.app_model_config

View File

@@ -0,0 +1,5 @@
from services.errors.base import BaseServiceError
class PluginInstallationForbiddenError(BaseServiceError):
pass

View File

@@ -88,6 +88,26 @@ class WebAppAuthModel(BaseModel):
allow_email_password_login: bool = False 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: PluginInstallationScope = PluginInstallationScope.ALL
# If True, restrict plugin installation to the marketplace only
# Equivalent to ForceEnablePluginVerification
restrict_to_marketplace_only: bool = False
class FeatureModel(BaseModel): class FeatureModel(BaseModel):
billing: BillingModel = BillingModel() billing: BillingModel = BillingModel()
education: EducationModel = EducationModel() education: EducationModel = EducationModel()
@@ -128,6 +148,7 @@ class SystemFeatureModel(BaseModel):
license: LicenseModel = LicenseModel() license: LicenseModel = LicenseModel()
branding: BrandingModel = BrandingModel() branding: BrandingModel = BrandingModel()
webapp_auth: WebAppAuthModel = WebAppAuthModel() webapp_auth: WebAppAuthModel = WebAppAuthModel()
plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel()
class FeatureService: class FeatureService:
@@ -291,3 +312,12 @@ class FeatureService:
features.license.workspaces.enabled = license_info["workspaces"]["enabled"] features.license.workspaces.enabled = license_info["workspaces"]["enabled"]
features.license.workspaces.limit = license_info["workspaces"]["limit"] features.license.workspaces.limit = license_info["workspaces"]["limit"]
features.license.workspaces.size = license_info["workspaces"]["used"] 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"
]

View File

@@ -17,11 +17,18 @@ from core.plugin.entities.plugin import (
PluginInstallation, PluginInstallation,
PluginInstallationSource, PluginInstallationSource,
) )
from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginListResponse, PluginUploadResponse from core.plugin.entities.plugin_daemon import (
PluginDecodeResponse,
PluginInstallTask,
PluginListResponse,
PluginVerification,
)
from core.plugin.impl.asset import PluginAssetManager from core.plugin.impl.asset import PluginAssetManager
from core.plugin.impl.debugging import PluginDebuggingClient from core.plugin.impl.debugging import PluginDebuggingClient
from core.plugin.impl.plugin import PluginInstaller from core.plugin.impl.plugin import PluginInstaller
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from services.errors.plugin import PluginInstallationForbiddenError
from services.feature_service import FeatureService, PluginInstallationScope
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -86,6 +93,42 @@ class PluginService:
logger.exception("failed to fetch latest plugin version") logger.exception("failed to fetch latest plugin version")
return result return result
@staticmethod
def _check_marketplace_only_permission():
"""
Check if the marketplace only permission is enabled
"""
features = FeatureService.get_system_features()
if features.plugin_installation_permission.restrict_to_marketplace_only:
raise PluginInstallationForbiddenError("Plugin installation is restricted to marketplace only")
@staticmethod
def _check_plugin_installation_scope(plugin_verification: Optional[PluginVerification]):
"""
Check the plugin installation scope
"""
features = FeatureService.get_system_features()
match features.plugin_installation_permission.plugin_installation_scope:
case PluginInstallationScope.OFFICIAL_ONLY:
if (
plugin_verification is None
or 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 is None or 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 @staticmethod
def get_debugging_key(tenant_id: str) -> str: def get_debugging_key(tenant_id: str) -> str:
""" """
@@ -208,6 +251,8 @@ class PluginService:
# check if plugin pkg is already downloaded # check if plugin pkg is already downloaded
manager = PluginInstaller() manager = PluginInstaller()
features = FeatureService.get_system_features()
try: try:
manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier) manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
# already downloaded, skip, and record install event # already downloaded, skip, and record install event
@@ -215,7 +260,14 @@ class PluginService:
except Exception: except Exception:
# plugin not installed, download and upload pkg # plugin not installed, download and upload pkg
pkg = download_plugin_pkg(new_plugin_unique_identifier) 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_scope(response.verification)
return manager.upgrade_plugin( return manager.upgrade_plugin(
tenant_id, tenant_id,
@@ -239,6 +291,7 @@ class PluginService:
""" """
Upgrade plugin with github Upgrade plugin with github
""" """
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() manager = PluginInstaller()
return manager.upgrade_plugin( return manager.upgrade_plugin(
tenant_id, tenant_id,
@@ -253,33 +306,43 @@ class PluginService:
) )
@staticmethod @staticmethod
def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginUploadResponse: def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginDecodeResponse:
""" """
Upload plugin package files Upload plugin package files
returns: plugin_unique_identifier returns: plugin_unique_identifier
""" """
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() 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,
)
return response
@staticmethod @staticmethod
def upload_pkg_from_github( def upload_pkg_from_github(
tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False
) -> PluginUploadResponse: ) -> PluginDecodeResponse:
""" """
Install plugin from github release package files, Install plugin from github release package files,
returns plugin_unique_identifier returns plugin_unique_identifier
""" """
PluginService._check_marketplace_only_permission()
pkg = download_with_size_limit( pkg = download_with_size_limit(
f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE
) )
features = FeatureService.get_system_features()
manager = PluginInstaller() manager = PluginInstaller()
return manager.upload_pkg( response = manager.upload_pkg(
tenant_id, tenant_id,
pkg, pkg,
verify_signature, verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only,
) )
return response
@staticmethod @staticmethod
def upload_bundle( def upload_bundle(
@@ -289,11 +352,15 @@ class PluginService:
Upload a plugin bundle and return the dependencies. Upload a plugin bundle and return the dependencies.
""" """
manager = PluginInstaller() manager = PluginInstaller()
PluginService._check_marketplace_only_permission()
return manager.upload_bundle(tenant_id, bundle, verify_signature) return manager.upload_bundle(tenant_id, bundle, verify_signature)
@staticmethod @staticmethod
def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]): def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() manager = PluginInstaller()
return manager.install_from_identifiers( return manager.install_from_identifiers(
tenant_id, tenant_id,
plugin_unique_identifiers, plugin_unique_identifiers,
@@ -307,6 +374,8 @@ class PluginService:
Install plugin from github release package files, Install plugin from github release package files,
returns plugin_unique_identifier returns plugin_unique_identifier
""" """
PluginService._check_marketplace_only_permission()
manager = PluginInstaller() manager = PluginInstaller()
return manager.install_from_identifiers( return manager.install_from_identifiers(
tenant_id, tenant_id,
@@ -322,28 +391,33 @@ class PluginService:
) )
@staticmethod @staticmethod
def fetch_marketplace_pkg( def fetch_marketplace_pkg(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration:
tenant_id: str, plugin_unique_identifier: str, verify_signature: bool = False
) -> PluginDeclaration:
""" """
Fetch marketplace package Fetch marketplace package
""" """
if not dify_config.MARKETPLACE_ENABLED: if not dify_config.MARKETPLACE_ENABLED:
raise ValueError("marketplace is not enabled") raise ValueError("marketplace is not enabled")
features = FeatureService.get_system_features()
manager = PluginInstaller() manager = PluginInstaller()
try: try:
declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
except Exception: except Exception:
pkg = download_plugin_pkg(plugin_unique_identifier) 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_scope(response.verification)
declaration = response.manifest
return declaration return declaration
@staticmethod @staticmethod
def install_from_marketplace_pkg( def install_from_marketplace_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]):
tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False
):
""" """
Install plugin from marketplace package files, Install plugin from marketplace package files,
returns installation task id returns installation task id
@@ -353,15 +427,26 @@ class PluginService:
manager = PluginInstaller() manager = PluginInstaller()
features = FeatureService.get_system_features()
# check if already downloaded # check if already downloaded
for plugin_unique_identifier in plugin_unique_identifiers: for plugin_unique_identifier in plugin_unique_identifiers:
try: try:
manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
plugin_decode_response = manager.decode_plugin_from_identifier(tenant_id, plugin_unique_identifier)
# check if the plugin is available to install
PluginService._check_plugin_installation_scope(plugin_decode_response.verification)
# already downloaded, skip # already downloaded, skip
except Exception: except Exception:
# plugin not installed, download and upload pkg # plugin not installed, download and upload pkg
pkg = download_plugin_pkg(plugin_unique_identifier) 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_scope(response.verification)
return manager.install_from_identifiers( return manager.install_from_identifiers(
tenant_id, tenant_id,