22
33from __future__ import annotations
44
5- from fastapi import APIRouter , Depends , File , HTTPException , UploadFile
5+ from fastapi import APIRouter , Depends , File , HTTPException , UploadFile , status
66from sqlalchemy .ext .asyncio import AsyncSession
77
88from backend .api .v1 .dify_imports import get_dify_graphon_client
99from backend .database import get_db
1010from backend .plugins .capability_catalog import build_plugin_node_catalog
1111from backend .plugins .dify_package import MAX_COMPRESSED_BYTES , DifyPackageError
1212from backend .schemas .common import ApiResponse
13- from backend .schemas .plugin import PluginInstallationRead , PluginNodeCatalogRead
13+ from backend .schemas .plugin import (
14+ PluginInstallationRead ,
15+ PluginInstallationUpdate ,
16+ PluginNodeCatalogRead ,
17+ )
18+ from backend .security .identity import RequestIdentity , get_request_identity
19+ from backend .security .workspace_rbac import (
20+ WorkspacePermission ,
21+ get_workspace_access ,
22+ require_permission ,
23+ )
1424from backend .services .plugin_registry_service import (
1525 PluginRegistryError ,
1626 delete_plugin_installation ,
1727 get_plugin_installation ,
1828 import_dify_plugin ,
1929 list_plugin_installations ,
30+ update_plugin_installation ,
2031)
2132from backend .workflow .dify_graphon_client import DifyGraphonClient
2233
2334router = APIRouter (prefix = "/plugins" , tags = ["plugins" ])
35+ workspace_router = APIRouter (prefix = "/workspaces" , tags = ["plugins" ])
36+
37+
38+ def require_platform_admin (
39+ identity : RequestIdentity = Depends (get_request_identity ),
40+ ) -> RequestIdentity :
41+ if not identity .is_platform_admin :
42+ raise HTTPException (status .HTTP_403_FORBIDDEN , "Platform Admin required" )
43+ return identity
2444
2545
2646@router .get ("" , response_model = ApiResponse [list [PluginInstallationRead ]])
@@ -79,8 +99,10 @@ async def get_plugin(
7999)
80100async def import_dify_plugin_file (
81101 file : UploadFile = File (...),
102+ identity : RequestIdentity = Depends (require_platform_admin ),
82103 db : AsyncSession = Depends (get_db ),
83104) -> ApiResponse [PluginInstallationRead ]:
105+ del identity
84106 filename = file .filename or "manifest.yaml"
85107 content = await file .read (MAX_COMPRESSED_BYTES + 1 )
86108 if len (content ) > MAX_COMPRESSED_BYTES :
@@ -106,15 +128,170 @@ async def import_dify_plugin_file(
106128@router .delete ("/{installation_id}" , response_model = ApiResponse [None ])
107129async def delete_plugin (
108130 installation_id : str ,
131+ identity : RequestIdentity = Depends (require_platform_admin ),
109132 db : AsyncSession = Depends (get_db ),
110133) -> ApiResponse [None ]:
134+ del identity
111135 try :
112136 await delete_plugin_installation (db , installation_id )
113137 except PluginRegistryError as exc :
114138 raise _registry_http_error (exc ) from exc
115139 return ApiResponse .ok (None )
116140
117141
142+ @workspace_router .get (
143+ "/{workspace_id}/plugins" ,
144+ response_model = ApiResponse [list [PluginInstallationRead ]],
145+ )
146+ async def list_workspace_plugins (
147+ workspace_id : str ,
148+ identity : RequestIdentity = Depends (get_request_identity ),
149+ db : AsyncSession = Depends (get_db ),
150+ graphon_client : DifyGraphonClient = Depends (get_dify_graphon_client ),
151+ ) -> ApiResponse [list [PluginInstallationRead ]]:
152+ access = await get_workspace_access (db , workspace_id , identity )
153+ require_permission (access , WorkspacePermission .READ )
154+ return ApiResponse .ok (
155+ await list_plugin_installations (
156+ db ,
157+ workspace_id = workspace_id ,
158+ dify_runtime_ready = await graphon_client .is_healthy (),
159+ )
160+ )
161+
162+
163+ @workspace_router .get (
164+ "/{workspace_id}/plugins/capabilities" ,
165+ response_model = ApiResponse [PluginNodeCatalogRead ],
166+ )
167+ async def list_workspace_plugin_capabilities (
168+ workspace_id : str ,
169+ identity : RequestIdentity = Depends (get_request_identity ),
170+ db : AsyncSession = Depends (get_db ),
171+ graphon_client : DifyGraphonClient = Depends (get_dify_graphon_client ),
172+ ) -> ApiResponse [PluginNodeCatalogRead ]:
173+ access = await get_workspace_access (db , workspace_id , identity )
174+ require_permission (access , WorkspacePermission .READ )
175+ installations = await list_plugin_installations (
176+ db ,
177+ workspace_id = workspace_id ,
178+ dify_runtime_ready = await graphon_client .is_healthy (),
179+ )
180+ return ApiResponse .ok (build_plugin_node_catalog (installations ))
181+
182+
183+ @workspace_router .get (
184+ "/{workspace_id}/plugins/{installation_id}" ,
185+ response_model = ApiResponse [PluginInstallationRead ],
186+ )
187+ async def get_workspace_plugin (
188+ workspace_id : str ,
189+ installation_id : str ,
190+ identity : RequestIdentity = Depends (get_request_identity ),
191+ db : AsyncSession = Depends (get_db ),
192+ graphon_client : DifyGraphonClient = Depends (get_dify_graphon_client ),
193+ ) -> ApiResponse [PluginInstallationRead ]:
194+ access = await get_workspace_access (db , workspace_id , identity )
195+ require_permission (access , WorkspacePermission .READ )
196+ installation = await get_plugin_installation (
197+ db ,
198+ installation_id ,
199+ workspace_id = workspace_id ,
200+ dify_runtime_ready = await graphon_client .is_healthy (),
201+ )
202+ if installation is None :
203+ raise _registry_http_error (
204+ PluginRegistryError (
205+ "plugin_installation_not_found" ,
206+ "Plugin installation not found." ,
207+ status_code = 404 ,
208+ )
209+ )
210+ return ApiResponse .ok (installation )
211+
212+
213+ @workspace_router .post (
214+ "/{workspace_id}/plugins/import/dify" ,
215+ response_model = ApiResponse [PluginInstallationRead ],
216+ status_code = status .HTTP_201_CREATED ,
217+ )
218+ async def import_workspace_dify_plugin_file (
219+ workspace_id : str ,
220+ file : UploadFile = File (...),
221+ identity : RequestIdentity = Depends (get_request_identity ),
222+ db : AsyncSession = Depends (get_db ),
223+ ) -> ApiResponse [PluginInstallationRead ]:
224+ access = await get_workspace_access (db , workspace_id , identity )
225+ require_permission (access , WorkspacePermission .MANAGE_CONFIGURATION )
226+ filename = file .filename or "manifest.yaml"
227+ content = await file .read (MAX_COMPRESSED_BYTES + 1 )
228+ if len (content ) > MAX_COMPRESSED_BYTES :
229+ raise HTTPException (
230+ status_code = 413 ,
231+ detail = {
232+ "code" : "dify_plugin_package_too_large" ,
233+ "message" : "The uploaded plugin package exceeds the 50 MiB compressed limit." ,
234+ },
235+ )
236+ try :
237+ installation = await import_dify_plugin (
238+ db , filename = filename , content = content , workspace_id = workspace_id
239+ )
240+ except DifyPackageError as exc :
241+ raise HTTPException (
242+ status_code = 422 ,
243+ detail = {"code" : exc .code , "message" : exc .message },
244+ ) from exc
245+ except PluginRegistryError as exc :
246+ raise _registry_http_error (exc ) from exc
247+ return ApiResponse .ok (installation )
248+
249+
250+ @workspace_router .patch (
251+ "/{workspace_id}/plugins/{installation_id}" ,
252+ response_model = ApiResponse [PluginInstallationRead ],
253+ )
254+ async def update_workspace_plugin (
255+ workspace_id : str ,
256+ installation_id : str ,
257+ payload : PluginInstallationUpdate ,
258+ identity : RequestIdentity = Depends (get_request_identity ),
259+ db : AsyncSession = Depends (get_db ),
260+ ) -> ApiResponse [PluginInstallationRead ]:
261+ access = await get_workspace_access (db , workspace_id , identity )
262+ require_permission (access , WorkspacePermission .MANAGE_CONFIGURATION )
263+ try :
264+ installation = await update_plugin_installation (
265+ db ,
266+ installation_id ,
267+ workspace_id = workspace_id ,
268+ enabled = payload .enabled ,
269+ granted_permissions = payload .granted_permissions ,
270+ )
271+ except PluginRegistryError as exc :
272+ raise _registry_http_error (exc ) from exc
273+ return ApiResponse .ok (installation )
274+
275+
276+ @workspace_router .delete (
277+ "/{workspace_id}/plugins/{installation_id}" ,
278+ response_model = ApiResponse [None ],
279+ )
280+ async def delete_workspace_plugin (
281+ workspace_id : str ,
282+ installation_id : str ,
283+ identity : RequestIdentity = Depends (get_request_identity ),
284+ db : AsyncSession = Depends (get_db ),
285+ ) -> ApiResponse [None ]:
286+ access = await get_workspace_access (db , workspace_id , identity )
287+ require_permission (access , WorkspacePermission .MANAGE_CONFIGURATION )
288+ try :
289+ await delete_plugin_installation (db , installation_id , workspace_id = workspace_id )
290+ except PluginRegistryError as exc :
291+ raise _registry_http_error (exc ) from exc
292+ return ApiResponse .ok (None )
293+
294+
118295def _registry_http_error (error : PluginRegistryError ) -> HTTPException :
119296 return HTTPException (
120297 status_code = error .status_code ,
0 commit comments