Kaynağa Gözat

Feat/plugin install scope management (#19963)

tags/1.5.0
Xin Zhang 4 ay önce
ebeveyn
işleme
30cfc9c172
No account linked to committer's email address

+ 15
- 1
api/core/plugin/entities/plugin_daemon.py Dosyayı Görüntüle

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):

+ 15
- 3
api/core/plugin/impl/plugin.py Dosyayı Görüntüle

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


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.
""" """
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,
) )
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]:

+ 1
- 1
api/models/model.py Dosyayı Görüntüle

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
@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

+ 5
- 0
api/services/errors/plugin.py Dosyayı Görüntüle

from services.errors.base import BaseServiceError


class PluginInstallationForbiddenError(BaseServiceError):
pass

+ 30
- 0
api/services/feature_service.py Dosyayı Görüntüle

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()
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:
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"
]

+ 100
- 15
api/services/plugin/plugin_service.py Dosyayı Görüntüle

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__)


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:
""" """
# 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
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,
""" """
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,
) )


@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(
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,
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,
) )


@staticmethod @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 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(
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, Install plugin from marketplace package files,
returns installation task id returns installation task id


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,

Loading…
İptal
Kaydet