Files
dify/api/services/plugin/installation_gateway.py
zyssyz123 ad0f3d3a49 fix(plugins): preserve admitted Marketplace package identifiers
Reject a downloaded or cached Marketplace package whose decoded immutable UID differs from the application-admitted request before any tenant installation or cache invalidation. Keep signature, scope, Marketplace provenance and source-admission policy unchanged.

This closes a source-version identity gap found while preparing transparent legacy credits forwarding. It prevents validating one provider package and silently installing another; original provider/App identities remain authoritative.

Reviewed local candidate 0aa5d04e72a641d7e9cc8e08d3c31701fb23f816; identical tree a2420679f4. Independent Gateway and neighboring admission tests passed (89), Ruff/type checks passed. Dev first; promote into the existing migration PR after live regression.
2026-11-12 14:42:59 +08:00

223 lines
9.1 KiB
Python

"""Legacy daemon mechanics behind the admitted plugin installation use case.
These five operations preserve existing download, signature/source-policy and
cache invalidation behavior. Application admission runs before this adapter;
there is deliberately no Kernel-to-application composition-root backedge.
"""
from collections.abc import Sequence
from configs import dify_config
from core.helper import marketplace
from core.helper.marketplace import download_plugin_pkg
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.entities.plugin_daemon import PluginInstallTaskStartResponse
from core.plugin.impl.plugin import PluginInstaller
from core.plugin.plugin_service import PluginService
from services.plugin.installation_service import PluginInstallationOperation, PluginInstallationResult
def _result(value: PluginInstallTaskStartResponse) -> PluginInstallationResult:
return PluginInstallationResult(
all_installed=value.all_installed,
task_id=value.task_id,
task=value.task.model_dump(mode="json") if value.task is not None else None,
)
class LegacyPluginInstallationGateway:
@staticmethod
def assert_available(
operation: PluginInstallationOperation, original_uid: str | None, candidate_uid: str | None
) -> None:
if operation in {"marketplace", "upgrade_marketplace"}:
if not dify_config.MARKETPLACE_ENABLED:
raise ValueError("marketplace is not enabled")
if operation == "upgrade_marketplace" and original_uid == candidate_uid:
raise ValueError("you should not upgrade plugin with the same plugin")
PluginService._get_plugin_installation_permission()
else:
PluginService._check_marketplace_only_permission()
@staticmethod
def upgrade_plugin_with_marketplace(
tenant_id: str, original_plugin_unique_identifier: str, new_plugin_unique_identifier: str
) -> PluginInstallationResult:
"""
Upgrade plugin with marketplace
"""
if not dify_config.MARKETPLACE_ENABLED:
raise ValueError("marketplace is not enabled")
if original_plugin_unique_identifier == new_plugin_unique_identifier:
raise ValueError("you should not upgrade plugin with the same plugin")
# check if plugin pkg is already downloaded
manager = PluginInstaller()
permission = PluginService._get_plugin_installation_permission()
try:
manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier)
except Exception:
# plugin not downloaded yet, download and upload pkg
pkg = download_plugin_pkg(new_plugin_unique_identifier)
response = manager.upload_pkg(
tenant_id,
pkg,
verify_signature=permission.restrict_to_marketplace_only,
)
# check if the plugin is available to install
PluginService._check_plugin_installation_scope(response.verification)
else:
# already downloaded, the cached pkg still has to satisfy the installation scope
decode_response = manager.decode_plugin_from_identifier(tenant_id, new_plugin_unique_identifier)
PluginService._check_plugin_installation_scope(decode_response.verification)
marketplace.record_install_plugin_event(new_plugin_unique_identifier)
result = manager.upgrade_plugin(
tenant_id,
original_plugin_unique_identifier,
new_plugin_unique_identifier,
PluginInstallationSource.Marketplace,
{
"plugin_unique_identifier": new_plugin_unique_identifier,
},
)
PluginService.invalidate_plugin_model_providers_cache(tenant_id)
return _result(result)
@staticmethod
def upgrade_plugin_with_github(
tenant_id: str,
original_plugin_unique_identifier: str,
new_plugin_unique_identifier: str,
repo: str,
version: str,
package: str,
) -> PluginInstallationResult:
"""
Upgrade plugin with github
"""
PluginService._check_marketplace_only_permission()
manager = PluginInstaller()
result = manager.upgrade_plugin(
tenant_id,
original_plugin_unique_identifier,
new_plugin_unique_identifier,
PluginInstallationSource.Github,
{
"repo": repo,
"version": version,
"package": package,
},
)
PluginService.invalidate_plugin_model_providers_cache(tenant_id)
return _result(result)
@staticmethod
def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]) -> PluginInstallationResult:
PluginService._check_marketplace_only_permission()
manager = PluginInstaller()
for plugin_unique_identifier in plugin_unique_identifiers:
resp = manager.decode_plugin_from_identifier(tenant_id, plugin_unique_identifier)
PluginService._check_plugin_installation_scope(resp.verification)
result = manager.install_from_identifiers(
tenant_id,
plugin_unique_identifiers,
PluginInstallationSource.Package,
[
{"plugin_unique_identifier": plugin_unique_identifier}
for plugin_unique_identifier in plugin_unique_identifiers
],
)
PluginService.invalidate_plugin_model_providers_cache(tenant_id)
return _result(result)
@staticmethod
def install_from_github(
tenant_id: str, plugin_unique_identifier: str, repo: str, version: str, package: str
) -> PluginInstallationResult:
"""
Install plugin from github release package files,
returns plugin_unique_identifier
"""
PluginService._check_marketplace_only_permission()
manager = PluginInstaller()
plugin_decode_response = manager.decode_plugin_from_identifier(tenant_id, plugin_unique_identifier)
PluginService._check_plugin_installation_scope(plugin_decode_response.verification)
result = manager.install_from_identifiers(
tenant_id,
[plugin_unique_identifier],
PluginInstallationSource.Github,
[
{
"repo": repo,
"version": version,
"package": package,
}
],
)
PluginService.invalidate_plugin_model_providers_cache(tenant_id)
return _result(result)
@staticmethod
def install_from_marketplace_pkg(
tenant_id: str, plugin_unique_identifiers: Sequence[str]
) -> PluginInstallationResult:
"""
Install plugin from marketplace package files,
returns installation task id
"""
if not dify_config.MARKETPLACE_ENABLED:
raise ValueError("marketplace is not enabled")
manager = PluginInstaller()
# Install only the exact identifiers admitted by the application owner.
actual_plugin_unique_identifiers = []
metas = []
permission = PluginService._get_plugin_installation_permission()
# check if already downloaded
for plugin_unique_identifier in plugin_unique_identifiers:
try:
manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier)
except Exception:
# plugin not downloaded yet, download and upload pkg
pkg = download_plugin_pkg(plugin_unique_identifier)
response = manager.upload_pkg(
tenant_id,
pkg,
verify_signature=permission.restrict_to_marketplace_only,
)
# check if the plugin is available to install
PluginService._check_plugin_installation_scope(response.verification)
if response.unique_identifier != plugin_unique_identifier:
raise ValueError(
"Marketplace package identifier does not match the requested identifier."
) from None
else:
# already downloaded, the cached pkg still has to satisfy the installation scope
plugin_decode_response = manager.decode_plugin_from_identifier(tenant_id, plugin_unique_identifier)
PluginService._check_plugin_installation_scope(plugin_decode_response.verification)
if plugin_decode_response.unique_identifier != plugin_unique_identifier:
raise ValueError("Marketplace package identifier does not match the requested identifier.")
actual_plugin_unique_identifiers.append(plugin_unique_identifier)
metas.append({"plugin_unique_identifier": plugin_unique_identifier})
result = manager.install_from_identifiers(
tenant_id,
actual_plugin_unique_identifiers,
PluginInstallationSource.Marketplace,
metas,
)
PluginService.invalidate_plugin_model_providers_cache(tenant_id)
return _result(result)