6161 get_optional_user ,
6262)
6363from langflow .services .authorization import FlowAction , ensure_flow_permission
64+ from langflow .services .authorization .access_ceiling import (
65+ external_access_allows ,
66+ get_current_external_access_context ,
67+ )
6468from langflow .services .cache .utils import save_uploaded_file
6569from langflow .services .database .models .flow .model import Flow , FlowRead
6670from langflow .services .database .models .flow .utils import get_all_webhook_components_in_flow
@@ -1226,6 +1230,7 @@ async def get_task_status(_task_id: str) -> TaskStatusResponse:
12261230async def create_upload_file (
12271231 file : UploadFile ,
12281232 flow : Annotated [Flow , Depends (get_flow )],
1233+ current_user : CurrentActiveUser ,
12291234 settings_service : Annotated [SettingsService , Depends (get_settings_service )],
12301235) -> UploadFileResponse :
12311236 """Upload a file for a specific flow (Deprecated).
@@ -1237,6 +1242,17 @@ async def create_upload_file(
12371242 ``/api/v1/files/upload/{flow_id}`` so authenticated callers can't fill
12381243 disk through this route either.
12391244 """
1245+ # Writing a file to a flow's storage is a flow mutation: enforce WRITE so
1246+ # the external access ceiling (e.g. a "viewer") cannot upload via this
1247+ # deprecated route. Mirrors the non-deprecated twin in files.py.
1248+ await ensure_flow_permission (
1249+ current_user ,
1250+ FlowAction .WRITE ,
1251+ flow_id = flow .id ,
1252+ flow_user_id = flow .user_id ,
1253+ workspace_id = flow .workspace_id ,
1254+ folder_id = flow .folder_id ,
1255+ )
12401256 try :
12411257 max_file_size_upload = settings_service .settings .max_file_size_upload
12421258 except Exception as exc :
@@ -1273,6 +1289,19 @@ async def custom_component(
12731289 user : CurrentActiveUser ,
12741290 request : Request ,
12751291) -> CustomComponentResponse :
1292+ # Building a custom component instantiates (and partially executes) posted
1293+ # code. That is a create/write-class action, so enforce the external access
1294+ # ceiling directly: a "viewer" external identity is denied while
1295+ # editor/admin (and all non-external users) pass unchanged. This route is
1296+ # not tied to a single owned resource, so the deny-only primitive is used
1297+ # instead of an ``ensure_*_permission`` guard.
1298+ external_context = get_current_external_access_context ()
1299+ if external_context is not None and not external_access_allows ("create" , external_context ):
1300+ raise HTTPException (
1301+ status_code = status .HTTP_403_FORBIDDEN ,
1302+ detail = "External credentials do not allow this action" ,
1303+ )
1304+
12761305 settings_service = get_settings_service ()
12771306 settings = settings_service .settings
12781307 all_known = None
@@ -1348,6 +1377,17 @@ async def custom_component_update(
13481377 HTTPException: If an error occurs during component building or updating.
13491378 SerializationError: If serialization of the updated component node fails.
13501379 """
1380+ # Updating a custom component instantiates (and partially executes) posted
1381+ # code, a create/write-class action. Enforce the external access ceiling
1382+ # directly so a "viewer" external identity is denied; editor/admin (and all
1383+ # non-external users) pass unchanged. Same action string as ``custom_component``.
1384+ external_context = get_current_external_access_context ()
1385+ if external_context is not None and not external_access_allows ("create" , external_context ):
1386+ raise HTTPException (
1387+ status_code = status .HTTP_403_FORBIDDEN ,
1388+ detail = "External credentials do not allow this action" ,
1389+ )
1390+
13511391 settings_service = get_settings_service ()
13521392 all_known = None
13531393 if _requires_component_hash_lookups (settings_service .settings , user ):
0 commit comments