refactor(auth): simplify flow retrieval by removing settings_service dependency (#8956)
* refactor: Simplify flow reading logic by removing unnecessary settings service dependency * 🛠️ (flows.py): Remove settings_service parameter from _read_flow and related functions to streamline flow retrieval logic. * 🔧 (flows.py): Adjust query to directly filter by user_id, enhancing clarity and reducing complexity. * test: Enhance flow retrieval tests with user isolation checks * 🧪 (test_flows.py): Remove hardcoded user_id from flow creation tests to ensure user-specific data handling. * ✨ (test_flows.py): Add a new test to verify that users can only access their own flows, ensuring proper user isolation in flow retrieval. * 🔧 (test_flows.py): Implement user creation and cleanup logic to maintain test integrity and avoid side effects.
This commit is contained in:
parent
73c1f203b0
commit
d437d018ce
2 changed files with 137 additions and 17 deletions
|
|
@ -36,7 +36,6 @@ from langflow.services.database.models.flow.utils import get_webhook_component_i
|
||||||
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
|
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
|
||||||
from langflow.services.database.models.folder.model import Folder
|
from langflow.services.database.models.folder.model import Folder
|
||||||
from langflow.services.deps import get_settings_service
|
from langflow.services.deps import get_settings_service
|
||||||
from langflow.services.settings.service import SettingsService
|
|
||||||
from langflow.utils.compression import compress_response
|
from langflow.utils.compression import compress_response
|
||||||
|
|
||||||
# build router
|
# build router
|
||||||
|
|
@ -270,17 +269,10 @@ async def _read_flow(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
flow_id: UUID,
|
flow_id: UUID,
|
||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
settings_service: SettingsService,
|
|
||||||
):
|
):
|
||||||
"""Read a flow."""
|
"""Read a flow."""
|
||||||
auth_settings = settings_service.auth_settings
|
stmt = select(Flow).where(Flow.id == flow_id).where(Flow.user_id == user_id)
|
||||||
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
|
|
||||||
)
|
|
||||||
return (await session.exec(stmt)).first()
|
return (await session.exec(stmt)).first()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -292,7 +284,7 @@ async def read_flow(
|
||||||
current_user: CurrentActiveUser,
|
current_user: CurrentActiveUser,
|
||||||
):
|
):
|
||||||
"""Read a flow."""
|
"""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
|
return user_flow
|
||||||
raise HTTPException(status_code=404, detail="Flow not found")
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
|
||||||
|
|
@ -327,7 +319,6 @@ async def update_flow(
|
||||||
session=session,
|
session=session,
|
||||||
flow_id=flow_id,
|
flow_id=flow_id,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
settings_service=settings_service,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not db_flow:
|
if not db_flow:
|
||||||
|
|
@ -393,7 +384,6 @@ async def delete_flow(
|
||||||
session=session,
|
session=session,
|
||||||
flow_id=flow_id,
|
flow_id=flow_id,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
settings_service=get_settings_service(),
|
|
||||||
)
|
)
|
||||||
if not flow:
|
if not flow:
|
||||||
raise HTTPException(status_code=404, detail="Flow not found")
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ async def test_create_flow(client: AsyncClient, logged_in_headers):
|
||||||
"webhook": False,
|
"webhook": False,
|
||||||
"endpoint_name": "string",
|
"endpoint_name": "string",
|
||||||
"tags": ["string"],
|
"tags": ["string"],
|
||||||
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
||||||
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
"fs_path": str(flow_file),
|
"fs_path": str(flow_file),
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +78,6 @@ async def test_read_flow(client: AsyncClient, logged_in_headers):
|
||||||
"webhook": False,
|
"webhook": False,
|
||||||
"endpoint_name": "string",
|
"endpoint_name": "string",
|
||||||
"tags": ["string"],
|
"tags": ["string"],
|
||||||
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
||||||
"folder_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)
|
response_ = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
|
||||||
|
|
@ -118,7 +116,6 @@ async def test_update_flow(client: AsyncClient, logged_in_headers):
|
||||||
"webhook": False,
|
"webhook": False,
|
||||||
"endpoint_name": "string",
|
"endpoint_name": "string",
|
||||||
"tags": ["string"],
|
"tags": ["string"],
|
||||||
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
||||||
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
}
|
}
|
||||||
basic_case["name"] = name
|
basic_case["name"] = name
|
||||||
|
|
@ -167,7 +164,6 @@ async def test_create_flows(client: AsyncClient, logged_in_headers):
|
||||||
"is_component": False,
|
"is_component": False,
|
||||||
"webhook": False,
|
"webhook": False,
|
||||||
"tags": ["string"],
|
"tags": ["string"],
|
||||||
"user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
||||||
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||||
}
|
}
|
||||||
cases = []
|
cases = []
|
||||||
|
|
@ -192,3 +188,137 @@ async def test_read_basic_examples(client: AsyncClient, logged_in_headers):
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert isinstance(result, list), "The result must be a list"
|
assert isinstance(result, list), "The result must be a list"
|
||||||
assert len(result) > 0, "The result must have at least one flow"
|
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…
Add table
Add a link
Reference in a new issue