From c1b914ee7cc73072fe4cdfff8d829a2fb7cfe66f Mon Sep 17 00:00:00 2001 From: Kailun Wang Date: Fri, 28 Nov 2025 18:31:26 -0500 Subject: [PATCH 1/3] fix: Auto-delete API credentials when uninstalling plugin Automatically remove plugin provider credentials on uninstall to prevent orphaned keys from being restored when plugin is reinstalled. Fixes #27531 --- api/controllers/console/workspace/plugin.py | 26 +++++++++++++++++ api/services/plugin/plugin_service.py | 31 +++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index 7e08ea55f9..c8fb916257 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -826,3 +826,29 @@ class PluginReadmeApi(Resource): return jsonable_encoder( {"readme": PluginService.fetch_plugin_readme(tenant_id, args.plugin_unique_identifier, args.language)} ) + + +class ParserUninstall(BaseModel): + plugin_installation_id: str = Field(..., description="Plugin installation ID") + + +console_ns.schema_model( + ParserUninstall.__name__, ParserUninstall.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0) +) + + +@console_ns.route("/workspaces/current/plugin/uninstall") +class PluginUninstallApi(Resource): + @console_ns.expect(console_ns.models[ParserUninstall.__name__]) + @setup_required + @login_required + @account_initialization_required + @plugin_permission_required(install_required=True) + def post(self): + _, tenant_id = current_account_with_tenant() + args = ParserUninstall.model_validate(console_ns.payload) + + try: + return {"success": PluginService.uninstall(tenant_id, args.plugin_installation_id)} + except PluginDaemonClientSideError as e: + raise ValueError(e) diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index b8303eb724..447ddbb5e8 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -505,7 +505,38 @@ class PluginService: @staticmethod def uninstall(tenant_id: str, plugin_installation_id: str) -> bool: + from extensions.ext_database import db + from models.provider import ProviderCredential + from sqlalchemy import select + manager = PluginInstaller() + + # Get plugin info before uninstalling to delete associated credentials + try: + plugins = manager.list_plugins(tenant_id) + plugin = next((p for p in plugins if p.installation_id == plugin_installation_id), None) + + if plugin: + plugin_id = plugin.plugin_id + logger.info(f"Deleting credentials for plugin: {plugin_id}") + + # Delete provider credentials that match this plugin + credentials = db.session.scalars( + select(ProviderCredential).where( + ProviderCredential.tenant_id == tenant_id, + ProviderCredential.provider_name.like(f"{plugin_id}/%"), + ) + ).all() + + for cred in credentials: + db.session.delete(cred) + + db.session.commit() + logger.info(f"Deleted {len(credentials)} credentials for plugin: {plugin_id}") + except Exception as e: + logger.warning(f"Failed to delete credentials: {e}") + # Continue with uninstall even if credential deletion fails + return manager.uninstall(tenant_id, plugin_installation_id) @staticmethod From 7be966b775cf796d17057fd29c9b38e38c6f4554 Mon Sep 17 00:00:00 2001 From: Kailun Wang Date: Sat, 29 Nov 2025 18:29:05 -0500 Subject: [PATCH 2/3] fix: CI issues - use lazy logging and remove duplicate class definitions --- api/controllers/console/workspace/plugin.py | 26 --------------------- api/services/plugin/plugin_service.py | 6 ++--- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index c8fb916257..7e08ea55f9 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -826,29 +826,3 @@ class PluginReadmeApi(Resource): return jsonable_encoder( {"readme": PluginService.fetch_plugin_readme(tenant_id, args.plugin_unique_identifier, args.language)} ) - - -class ParserUninstall(BaseModel): - plugin_installation_id: str = Field(..., description="Plugin installation ID") - - -console_ns.schema_model( - ParserUninstall.__name__, ParserUninstall.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0) -) - - -@console_ns.route("/workspaces/current/plugin/uninstall") -class PluginUninstallApi(Resource): - @console_ns.expect(console_ns.models[ParserUninstall.__name__]) - @setup_required - @login_required - @account_initialization_required - @plugin_permission_required(install_required=True) - def post(self): - _, tenant_id = current_account_with_tenant() - args = ParserUninstall.model_validate(console_ns.payload) - - try: - return {"success": PluginService.uninstall(tenant_id, args.plugin_installation_id)} - except PluginDaemonClientSideError as e: - raise ValueError(e) diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index 447ddbb5e8..da02ce6642 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -518,7 +518,7 @@ class PluginService: if plugin: plugin_id = plugin.plugin_id - logger.info(f"Deleting credentials for plugin: {plugin_id}") + logger.info("Deleting credentials for plugin: %s", plugin_id) # Delete provider credentials that match this plugin credentials = db.session.scalars( @@ -532,9 +532,9 @@ class PluginService: db.session.delete(cred) db.session.commit() - logger.info(f"Deleted {len(credentials)} credentials for plugin: {plugin_id}") + logger.info("Deleted %d credentials for plugin: %s", len(credentials), plugin_id) except Exception as e: - logger.warning(f"Failed to delete credentials: {e}") + logger.warning("Failed to delete credentials: %s", e) # Continue with uninstall even if credential deletion fails return manager.uninstall(tenant_id, plugin_installation_id) From e64caca697bdfa8dd2809f334d5cccbdf31e145c Mon Sep 17 00:00:00 2001 From: Kailun Wang Date: Sun, 30 Nov 2025 18:46:50 -0500 Subject: [PATCH 3/3] refactor: move imports to top level in plugin_service.py - Move sqlalchemy, db, and ProviderCredential imports to file top - Remove local imports from uninstall() method - Addresses reviewer feedback --- api/services/plugin/plugin_service.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index da02ce6642..411c335c17 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -3,6 +3,7 @@ from collections.abc import Mapping, Sequence from mimetypes import guess_type from pydantic import BaseModel +from sqlalchemy import select from yarl import URL from configs import dify_config @@ -25,7 +26,9 @@ from core.plugin.entities.plugin_daemon import ( from core.plugin.impl.asset import PluginAssetManager from core.plugin.impl.debugging import PluginDebuggingClient from core.plugin.impl.plugin import PluginInstaller +from extensions.ext_database import db from extensions.ext_redis import redis_client +from models.provider import ProviderCredential from models.provider_ids import GenericProviderID from services.errors.plugin import PluginInstallationForbiddenError from services.feature_service import FeatureService, PluginInstallationScope @@ -505,10 +508,6 @@ class PluginService: @staticmethod def uninstall(tenant_id: str, plugin_installation_id: str) -> bool: - from extensions.ext_database import db - from models.provider import ProviderCredential - from sqlalchemy import select - manager = PluginInstaller() # Get plugin info before uninstalling to delete associated credentials