Skip to content

Commit 55f2328

Browse files
committed
merge fix
2 parents 4e0f3ad + 4f86736 commit 55f2328

59 files changed

Lines changed: 1850 additions & 8978 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/docs/API-Reference/workflows-api.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ The API uses standard HTTP status codes to indicate success or failure:
504504
|-------------|-------------|
505505
| `200 OK` | Request successful. |
506506
| `400 Bad Request` | Invalid request parameters. |
507-
| `401 Unauthorized` | Invalid or missing API key. |
507+
| `403 Forbidden` | Invalid or missing API key. |
508508
| `404 Not Found` | Flow not found or developer API disabled. |
509509
| `422 Unprocessable Entity` | Unknown `stream_protocol` or unsupported field for the selected mode. |
510510
| `500 Internal Server Error` | Server error during execution. |

src/backend/base/langflow/agentic/flows/LangflowAssistant.json

Lines changed: 342 additions & 641 deletions
Large diffs are not rendered by default.

src/backend/base/langflow/agentic/flows/SystemMessageGen.json

Lines changed: 0 additions & 3287 deletions
This file was deleted.

src/backend/base/langflow/agentic/flows/TemplateAssistant.json

Lines changed: 0 additions & 4285 deletions
This file was deleted.

src/backend/base/langflow/agentic/flows/TranslationFlow.json

Lines changed: 210 additions & 438 deletions
Large diffs are not rendered by default.

src/backend/base/langflow/api/v1/authz_shares.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
from langflow.services.authorization import ShareAction, ensure_share_permission
1717
from langflow.services.authorization.invalidation import safe_invalidate_all, safe_invalidate_user
1818
from langflow.services.authorization.utils import audit_decision
19-
from langflow.services.database.models.auth import AuthzShare, AuthzTeamMember, SharePermissionLevel, ShareScope
19+
from langflow.services.database.models.auth import (
20+
AuthzShare,
21+
AuthzTeam,
22+
AuthzTeamMember,
23+
SharePermissionLevel,
24+
ShareScope,
25+
)
2026
from langflow.services.database.models.deployment.model import Deployment
2127
from langflow.services.database.models.file.model import File as UserFile
2228
from langflow.services.database.models.flow.model import Flow
@@ -57,6 +63,38 @@ async def _resolve_resource_owner(
5763
return getattr(row, owner_attr, None)
5864

5965

66+
async def _serialize_shares(session: DbSession, rows: list[AuthzShare]) -> list[ShareRead]:
67+
"""Serialize shares with human-readable user and team target names.
68+
69+
``target_id`` is polymorphic, so the database cannot expose a normal ORM
70+
relationship. Resolve each target kind in one query and keep the name
71+
optional for deleted or otherwise stale targets.
72+
"""
73+
user_ids = {row.target_id for row in rows if row.scope == ShareScope.USER.value and row.target_id is not None}
74+
team_ids = {row.target_id for row in rows if row.scope == ShareScope.TEAM.value and row.target_id is not None}
75+
76+
user_names: dict[UUID, str] = {}
77+
if user_ids:
78+
user_rows = await session.exec(select(User.id, User.username).where(User.id.in_(user_ids)))
79+
user_names.update(dict(user_rows.all()))
80+
team_names: dict[UUID, str] = {}
81+
if team_ids:
82+
team_rows = await session.exec(select(AuthzTeam.id, AuthzTeam.team_name).where(AuthzTeam.id.in_(team_ids)))
83+
team_names.update(dict(team_rows.all()))
84+
85+
def target_name(row: AuthzShare) -> str | None:
86+
if row.scope == ShareScope.USER.value:
87+
return user_names.get(row.target_id)
88+
if row.scope == ShareScope.TEAM.value:
89+
return team_names.get(row.target_id)
90+
return None
91+
92+
return [
93+
ShareRead.model_validate(row, from_attributes=True).model_copy(update={"target_name": target_name(row)})
94+
for row in rows
95+
]
96+
97+
6098
def _share_visible(
6199
*,
62100
row: AuthzShare,
@@ -210,7 +248,7 @@ async def create_share(
210248
detail="Share could not be created: it may already exist or conflict with an existing share.",
211249
) from exc
212250
await session.refresh(row)
213-
response = ShareRead.model_validate(row, from_attributes=True)
251+
response = (await _serialize_shares(session, [row]))[0]
214252
await session.commit()
215253

216254
# Refresh policy after commit so plugins using a separate DB connection see
@@ -277,14 +315,14 @@ async def list_shares(
277315

278316
is_superuser = getattr(current_user, "is_superuser", False)
279317
if is_superuser:
280-
return [ShareRead.model_validate(row, from_attributes=True) for row in rows]
318+
return await _serialize_shares(session, rows)
281319

282320
# Pre-fetch team memberships (avoid N+1 per row).
283321
team_membership_stmt = select(AuthzTeamMember.team_id).where(AuthzTeamMember.user_id == current_user.id)
284322
caller_team_ids: set[UUID] = set(await session.exec(team_membership_stmt))
285323

286324
# Filter rows by visibility rules for non-superusers.
287-
visible: list[ShareRead] = []
325+
visible: list[AuthzShare] = []
288326
owner_cache: dict[tuple[str, UUID], UUID | None] = {}
289327
for row in rows:
290328
key = (row.resource_type, row.resource_id)
@@ -300,8 +338,8 @@ async def list_shares(
300338
resource_owner_id=owner_cache[key],
301339
caller_team_ids=caller_team_ids,
302340
):
303-
visible.append(ShareRead.model_validate(row, from_attributes=True))
304-
return visible
341+
visible.append(row)
342+
return await _serialize_shares(session, visible)
305343

306344

307345
def _row_visible_to(
@@ -355,7 +393,7 @@ async def get_share(
355393
# UUID privacy: forbidden share → 404.
356394
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Share not found")
357395

358-
return ShareRead.model_validate(row, from_attributes=True)
396+
return (await _serialize_shares(session, [row]))[0]
359397

360398

361399
@router.patch("/{share_id}", response_model=ShareRead)
@@ -405,7 +443,7 @@ async def update_share(
405443
detail="Share could not be updated: it may conflict with an existing share.",
406444
) from exc
407445
await session.refresh(row)
408-
response = ShareRead.model_validate(row, from_attributes=True)
446+
response = (await _serialize_shares(session, [row]))[0]
409447
await session.commit()
410448

411449
await _refresh_policy_for_share(response.scope, response.target_id, op="share:update")

src/backend/base/langflow/api/v1/memories.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,8 @@ async def create_memory_base(
104104
- Returns 422 if preprocessing=true but preproc_model is missing.
105105
"""
106106
# Memory bases are knowledge-base-backed resources; guard via the KB family.
107-
# Passing the actor as owner makes the owner-override path return early when
108-
# no access ceiling is set, preserving existing behavior, while a deny-only
109-
# external ceiling (e.g. a "viewer") is still enforced.
107+
# The actor is only the prospective owner during CREATE, so the shared guard
108+
# still evaluates policy instead of applying the existing-resource override.
110109
await ensure_knowledge_base_permission(
111110
current_user,
112111
KnowledgeBaseAction.CREATE,

src/backend/base/langflow/api/v1/schemas/authz_shares.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class ShareRead(BaseModel):
5858
resource_id: UUID
5959
scope: ShareScopeLiteral
6060
target_id: UUID | None
61+
target_name: str | None = None
6162
permission_level: SharePermissionLiteral
6263
created_by: UUID | None
6364
created_at: datetime

src/backend/base/langflow/api/v2/workflow.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from langflow.api.v2.workflow_validation import (
7171
_enforce_flow_data_override_owner,
7272
_flow_not_found_privacy_exception,
73+
_reject_sync_only_fields,
7374
_reject_unsupported_sync_fields,
7475
_validate_flow_data_for_execution,
7576
)
@@ -223,6 +224,7 @@ async def authorize_flow_action(
223224
def _apply_execution_gates(parsed, flow, current_user: UserRead) -> None:
224225
"""The langflow request gates that run before a flow executes."""
225226
_reject_unsupported_sync_fields(parsed)
227+
_reject_sync_only_fields(parsed)
226228
try:
227229
_enforce_flow_data_override_owner(parsed, flow, current_user)
228230
except HTTPException as exc:

src/backend/base/langflow/api/v2/workflow_validation.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,27 @@ def _reject_unsupported_sync_fields(parsed: ParsedWorkflowRun) -> None:
5050
)
5151

5252

53+
def _reject_sync_only_fields(parsed: ParsedWorkflowRun) -> None:
54+
"""Reject sync-only request fields in stream/background mode.
55+
56+
``output_ids`` only drives answer selection on the inline sync path; other
57+
modes would silently ignore it, which reads as success to the caller.
58+
"""
59+
if parsed.mode == "sync" or not parsed.output_ids:
60+
return
61+
62+
raise HTTPException(
63+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
64+
detail={
65+
"error": "Unsupported request fields for mode",
66+
"code": "MODE_UNSUPPORTED_FIELDS",
67+
"message": f"mode='{parsed.mode}' does not support request fields: output_ids. "
68+
"Output selection via output_ids only applies to mode='sync'.",
69+
"fields": ["output_ids"],
70+
},
71+
)
72+
73+
5374
def _enforce_flow_data_override_owner(parsed: ParsedWorkflowRun, flow: FlowRead, current_user: UserRead) -> None:
5475
"""Only the flow owner may execute caller-supplied graph data."""
5576
if parsed.data is None or flow.user_id == current_user.id:

0 commit comments

Comments
 (0)