From c1b914ee7cc73072fe4cdfff8d829a2fb7cfe66f Mon Sep 17 00:00:00 2001 From: Kailun Wang Date: Fri, 28 Nov 2025 18:31:26 -0500 Subject: [PATCH] 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