Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions src/backend/base/langflow/api/v1/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
from langflow.services.database.models.folder.model import Folder
from langflow.services.deps import get_settings_service
from langflow.services.settings.service import SettingsService
from langflow.utils.compression import compress_response

# build router
Expand Down Expand Up @@ -270,17 +269,10 @@ async def _read_flow(
session: AsyncSession,
flow_id: UUID,
user_id: UUID,
settings_service: SettingsService,
):
"""Read a flow."""
auth_settings = settings_service.auth_settings
stmt = select(Flow).where(Flow.id == flow_id)
if auth_settings.AUTO_LOGIN:
# If auto login is enable user_id can be current_user.id or None
# so write an OR
stmt = stmt.where(
(Flow.user_id == user_id) | (Flow.user_id == None) # noqa: E711
)
stmt = select(Flow).where(Flow.id == flow_id).where(Flow.user_id == user_id)

return (await session.exec(stmt)).first()


Expand All @@ -292,7 +284,7 @@ async def read_flow(
current_user: CurrentActiveUser,
):
"""Read a flow."""
if user_flow := await _read_flow(session, flow_id, current_user.id, get_settings_service()):
if user_flow := await _read_flow(session, flow_id, current_user.id):
return user_flow
raise HTTPException(status_code=404, detail="Flow not found")

Expand Down Expand Up @@ -327,7 +319,6 @@ async def update_flow(
session=session,
flow_id=flow_id,
user_id=current_user.id,
settings_service=settings_service,
)

if not db_flow:
Expand Down Expand Up @@ -393,7 +384,6 @@ async def delete_flow(
session=session,
flow_id=flow_id,
user_id=current_user.id,
settings_service=get_settings_service(),
)
if not flow:
raise HTTPException(status_code=404, detail="Flow not found")
Expand Down
138 changes: 134 additions & 4 deletions src/backend/tests/unit/api/v1/test_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ async def test_create_flow(client: AsyncClient, logged_in_headers):
"webhook": False,
"endpoint_name": "string",
"tags": ["string"],
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fs_path": str(flow_file),
}
Expand Down Expand Up @@ -79,7 +78,6 @@ async def test_read_flow(client: AsyncClient, logged_in_headers):
"webhook": False,
"endpoint_name": "string",
"tags": ["string"],
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}
response_ = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
Expand Down Expand Up @@ -118,7 +116,6 @@ async def test_update_flow(client: AsyncClient, logged_in_headers):
"webhook": False,
"endpoint_name": "string",
"tags": ["string"],
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}
basic_case["name"] = name
Expand Down Expand Up @@ -167,7 +164,6 @@ async def test_create_flows(client: AsyncClient, logged_in_headers):
"is_component": False,
"webhook": False,
"tags": ["string"],
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}
cases = []
Expand All @@ -192,3 +188,137 @@ async def test_read_basic_examples(client: AsyncClient, logged_in_headers):
assert response.status_code == status.HTTP_200_OK
assert isinstance(result, list), "The result must be a list"
assert len(result) > 0, "The result must have at least one flow"


async def test_read_flows_user_isolation(client: AsyncClient, logged_in_headers, active_user):
"""Test that read_flows returns only flows from the current user."""
from uuid import uuid4

from langflow.services.auth.utils import get_password_hash
from langflow.services.database.models.user.model import User
from langflow.services.deps import session_scope

# Create a second user
other_user_id = uuid4()
async with session_scope() as session:
other_user = User(
id=other_user_id,
username="other_test_user",
password=get_password_hash("testpassword"),
is_active=True,
is_superuser=False,
)
session.add(other_user)
await session.commit()
await session.refresh(other_user)

# Login as the other user to get headers
login_data = {"username": "other_test_user", "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
assert response.status_code == 200
tokens = response.json()
other_user_headers = {"Authorization": f"Bearer {tokens['access_token']}"}

# Create flows for the first user (active_user)
flow_user1_1 = {
"name": "user1_flow_1",
"description": "Flow 1 for user 1",
"icon": "string",
"icon_bg_color": "#ff00ff",
"gradient": "string",
"data": {},
"is_component": False,
"webhook": False,
"endpoint_name": "user1_flow_1_endpoint",
"tags": ["user1"],
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}

flow_user1_2 = {
"name": "user1_flow_2",
"description": "Flow 2 for user 1",
"icon": "string",
"icon_bg_color": "#00ff00",
"gradient": "string",
"data": {},
"is_component": False,
"webhook": False,
"endpoint_name": "user1_flow_2_endpoint",
"tags": ["user1"],
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}

# Create flows for the second user
flow_user2_1 = {
"name": "user2_flow_1",
"description": "Flow 1 for user 2",
"icon": "string",
"icon_bg_color": "#0000ff",
"gradient": "string",
"data": {},
"is_component": False,
"webhook": False,
"endpoint_name": "user2_flow_1_endpoint",
"tags": ["user2"],
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}

# Create flows using the appropriate user headers
response1 = await client.post("api/v1/flows/", json=flow_user1_1, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED

response2 = await client.post("api/v1/flows/", json=flow_user1_2, headers=logged_in_headers)
assert response2.status_code == status.HTTP_201_CREATED

response3 = await client.post("api/v1/flows/", json=flow_user2_1, headers=other_user_headers)
assert response3.status_code == status.HTTP_201_CREATED

# Test read_flows for user 1 - should only return user 1's flows
params = {
"remove_example_flows": True, # Exclude example flows to focus on our test flows
"components_only": False,
"get_all": True,
"header_flows": False,
"page": 1,
"size": 50,
}

response_user1 = await client.get("api/v1/flows/", params=params, headers=logged_in_headers)
result_user1 = response_user1.json()

assert response_user1.status_code == status.HTTP_200_OK
assert isinstance(result_user1, list), "The result must be a list"

# Verify only user 1's flows are returned
user1_flow_names = [flow["name"] for flow in result_user1]
assert "user1_flow_1" in user1_flow_names, "User 1's first flow should be returned"
assert "user1_flow_2" in user1_flow_names, "User 1's second flow should be returned"
assert "user2_flow_1" not in user1_flow_names, "User 2's flow should not be returned for user 1"

# Verify all returned flows belong to user 1
for flow in result_user1:
assert str(flow["user_id"]) == str(active_user.id), f"Flow {flow['name']} should belong to user 1"

# Test read_flows for user 2 - should only return user 2's flows
response_user2 = await client.get("api/v1/flows/", params=params, headers=other_user_headers)
result_user2 = response_user2.json()

assert response_user2.status_code == status.HTTP_200_OK
assert isinstance(result_user2, list), "The result must be a list"

# Verify only user 2's flows are returned
user2_flow_names = [flow["name"] for flow in result_user2]
assert "user2_flow_1" in user2_flow_names, "User 2's flow should be returned"
assert "user1_flow_1" not in user2_flow_names, "User 1's first flow should not be returned for user 2"
assert "user1_flow_2" not in user2_flow_names, "User 1's second flow should not be returned for user 2"

# Verify all returned flows belong to user 2
for flow in result_user2:
assert str(flow["user_id"]) == str(other_user_id), f"Flow {flow['name']} should belong to user 2"

# Cleanup: Delete the other user
async with session_scope() as session:
user = await session.get(User, other_user_id)
if user:
await session.delete(user)
await session.commit()
Loading