mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 13:56:04 +08:00
fix: Add authentication to various endpoints (#10977)
* fix: Add authentication to various endpoints * [autofix.ci] apply automated fixes * Couple more endpoints * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * Update log_router.py * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * Update mcp.py * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Fix ruff errors * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * Update test_endpoints.py * Fix tests * Update Nvidia Remix.json * Update test_registration.py * [autofix.ci] apply automated fixes * Update test_files.py Co-Authored-By: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * Address review comments Co-Authored-By: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * Review updates Co-Authored-By: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -3,10 +3,12 @@ import json
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from lfx.log.logger import log_buffer
|
||||
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
|
||||
log_router = APIRouter(tags=["Log"])
|
||||
|
||||
|
||||
@ -50,12 +52,13 @@ async def event_generator(request: Request):
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@log_router.get("/logs-stream")
|
||||
@log_router.get("/logs-stream", dependencies=[Depends(get_current_active_user)])
|
||||
async def stream_logs(
|
||||
request: Request,
|
||||
):
|
||||
"""HTTP/2 Server-Sent-Event (SSE) endpoint for streaming logs.
|
||||
|
||||
Requires authentication to prevent exposure of sensitive log data.
|
||||
It establishes a long-lived connection to the server and receives log messages in real-time.
|
||||
The client should use the header "Accept: text/event-stream".
|
||||
"""
|
||||
@ -69,12 +72,16 @@ async def stream_logs(
|
||||
return StreamingResponse(event_generator(request), media_type="text/event-stream")
|
||||
|
||||
|
||||
@log_router.get("/logs")
|
||||
@log_router.get("/logs", dependencies=[Depends(get_current_active_user)])
|
||||
async def logs(
|
||||
lines_before: Annotated[int, Query(description="The number of logs before the timestamp or the last log")] = 0,
|
||||
lines_after: Annotated[int, Query(description="The number of logs after the timestamp")] = 0,
|
||||
timestamp: Annotated[int, Query(description="The timestamp to start getting logs from")] = 0,
|
||||
):
|
||||
"""Retrieve application logs with authentication required.
|
||||
|
||||
SECURITY: Logs may contain sensitive information and require authentication.
|
||||
"""
|
||||
global log_buffer # noqa: PLW0602
|
||||
if log_buffer.enabled() is False:
|
||||
raise HTTPException(
|
||||
|
||||
@ -37,6 +37,7 @@ from langflow.api.v1.schemas import (
|
||||
VerticesOrderResponse,
|
||||
)
|
||||
from langflow.exceptions.component import ComponentBuildError
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.database.models.flow.model import Flow
|
||||
from langflow.services.deps import (
|
||||
@ -54,7 +55,7 @@ if TYPE_CHECKING:
|
||||
router = APIRouter(tags=["Chat"])
|
||||
|
||||
|
||||
@router.post("/build/{flow_id}/vertices", deprecated=True)
|
||||
@router.post("/build/{flow_id}/vertices", deprecated=True, dependencies=[Depends(get_current_active_user)])
|
||||
async def retrieve_vertices_order(
|
||||
*,
|
||||
flow_id: uuid.UUID,
|
||||
@ -197,14 +198,17 @@ async def build_flow(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/build/{job_id}/events")
|
||||
@router.get("/build/{job_id}/events", dependencies=[Depends(get_current_active_user)])
|
||||
async def get_build_events(
|
||||
job_id: str,
|
||||
queue_service: Annotated[JobQueueService, Depends(get_queue_service)],
|
||||
*,
|
||||
event_delivery: EventDeliveryType = EventDeliveryType.STREAMING,
|
||||
):
|
||||
"""Get events for a specific build job."""
|
||||
"""Get events for a specific build job.
|
||||
|
||||
Requires authentication to prevent unauthorized access to build events.
|
||||
"""
|
||||
return await get_flow_events_response(
|
||||
job_id=job_id,
|
||||
queue_service=queue_service,
|
||||
@ -212,12 +216,19 @@ async def get_build_events(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/build/{job_id}/cancel", response_model=CancelFlowResponse)
|
||||
@router.post(
|
||||
"/build/{job_id}/cancel",
|
||||
response_model=CancelFlowResponse,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
)
|
||||
async def cancel_build(
|
||||
job_id: str,
|
||||
queue_service: Annotated[JobQueueService, Depends(get_queue_service)],
|
||||
):
|
||||
"""Cancel a specific build job."""
|
||||
"""Cancel a specific build job.
|
||||
|
||||
Requires authentication to prevent unauthorized build cancellation.
|
||||
"""
|
||||
try:
|
||||
# Cancel the flow build and check if it was successful
|
||||
cancellation_success = await cancel_flow_build(job_id=job_id, queue_service=queue_service)
|
||||
@ -509,6 +520,7 @@ async def _stream_vertex(flow_id: str, vertex_id: str, chat_service: ChatService
|
||||
"/build/{flow_id}/{vertex_id}/stream",
|
||||
response_class=StreamingResponse,
|
||||
deprecated=True,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
)
|
||||
async def build_vertex_stream(
|
||||
flow_id: uuid.UUID,
|
||||
|
||||
@ -961,10 +961,12 @@ async def custom_component_update(
|
||||
raise SerializationError.from_exception(exc, data=component_node) from exc
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
@router.get("/config", dependencies=[Depends(get_current_active_user)])
|
||||
async def get_config() -> ConfigResponse:
|
||||
"""Retrieve the current application configuration settings.
|
||||
|
||||
Requires authentication to prevent exposure of sensitive configuration details.
|
||||
|
||||
Returns:
|
||||
ConfigResponse: The configuration settings of the application.
|
||||
|
||||
|
||||
@ -21,6 +21,41 @@ from langflow.services.storage.service import StorageService
|
||||
router = APIRouter(tags=["Files"], prefix="/files")
|
||||
|
||||
|
||||
def _get_allowed_profile_picture_folders(settings_service: SettingsService) -> set[str]:
|
||||
"""Return the set of allowed profile picture folders.
|
||||
|
||||
This enumerates subdirectories under the profile_pictures directory in both
|
||||
the user's config_dir and the package's bundled assets. This makes the API
|
||||
flexible (users may add new folders under config_dir/profile_pictures) while
|
||||
still safe because we only ever serve files contained within the resolved
|
||||
base directory and validate path containment below.
|
||||
|
||||
If no directories can be found (unexpected), fall back to the curated
|
||||
defaults {"People", "Space"} shipped with Langflow.
|
||||
"""
|
||||
allowed: set[str] = set()
|
||||
try:
|
||||
# Config-provided folders
|
||||
config_dir = Path(settings_service.settings.config_dir)
|
||||
cfg_base = config_dir / "profile_pictures"
|
||||
if cfg_base.exists():
|
||||
allowed.update({p.name for p in cfg_base.iterdir() if p.is_dir()})
|
||||
# Package-provided folders
|
||||
from langflow.initial_setup import setup
|
||||
|
||||
pkg_base = Path(setup.__file__).parent / "profile_pictures"
|
||||
if pkg_base.exists():
|
||||
allowed.update({p.name for p in pkg_base.iterdir() if p.is_dir()})
|
||||
except Exception as _:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception("Exception occurred while getting allowed profile picture folders")
|
||||
|
||||
# Sensible defaults ensure tests and OOTB behavior
|
||||
return allowed or {"People", "Space"}
|
||||
|
||||
|
||||
# Create dep that gets the flow_id from the request
|
||||
# then finds it in the database and returns it while
|
||||
# using the current user as the owner
|
||||
@ -31,10 +66,9 @@ async def get_flow(
|
||||
):
|
||||
# AttributeError: 'SelectOfScalar' object has no attribute 'first'
|
||||
flow = await session.get(Flow, flow_id)
|
||||
if not flow:
|
||||
# Return 404 for both non-existent flows and unauthorized access to prevent information disclosure
|
||||
if not flow or flow.user_id != current_user.id:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
if flow.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="You don't have access to this flow")
|
||||
return flow
|
||||
|
||||
|
||||
@ -43,7 +77,6 @@ async def upload_file(
|
||||
*,
|
||||
file: UploadFile,
|
||||
flow: Annotated[Flow, Depends(get_flow)],
|
||||
current_user: CurrentActiveUser,
|
||||
storage_service: Annotated[StorageService, Depends(get_storage_service)],
|
||||
settings_service: Annotated[SettingsService, Depends(get_settings_service)],
|
||||
) -> UploadFileResponse:
|
||||
@ -57,9 +90,7 @@ async def upload_file(
|
||||
status_code=413, detail=f"File size is larger than the maximum file size {max_file_size_upload}MB."
|
||||
)
|
||||
|
||||
if flow.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="You don't have access to this flow")
|
||||
|
||||
# Authorization handled by get_flow dependency
|
||||
try:
|
||||
file_content = await file.read()
|
||||
timestamp = datetime.now(tz=timezone.utc).astimezone().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
@ -74,9 +105,12 @@ async def upload_file(
|
||||
|
||||
@router.get("/download/{flow_id}/{file_name}")
|
||||
async def download_file(
|
||||
file_name: str, flow_id: UUID, storage_service: Annotated[StorageService, Depends(get_storage_service)]
|
||||
file_name: str,
|
||||
flow: Annotated[Flow, Depends(get_flow)],
|
||||
storage_service: Annotated[StorageService, Depends(get_storage_service)],
|
||||
):
|
||||
flow_id_str = str(flow_id)
|
||||
# Authorization handled by get_flow dependency
|
||||
flow_id_str = str(flow.id)
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
if not extension:
|
||||
@ -102,10 +136,14 @@ async def download_file(
|
||||
|
||||
|
||||
@router.get("/images/{flow_id}/{file_name}")
|
||||
async def download_image(file_name: str, flow_id: UUID):
|
||||
async def download_image(
|
||||
file_name: str,
|
||||
flow: Annotated[Flow, Depends(get_flow)],
|
||||
):
|
||||
# Authorization handled by get_flow dependency
|
||||
storage_service = get_storage_service()
|
||||
extension = file_name.split(".")[-1]
|
||||
flow_id_str = str(flow_id)
|
||||
flow_id_str = str(flow.id)
|
||||
|
||||
if not extension:
|
||||
raise HTTPException(status_code=500, detail=f"Extension not found for file {file_name}")
|
||||
@ -138,16 +176,49 @@ async def download_profile_picture(
|
||||
then fallback to the package's bundled profile_pictures directory.
|
||||
"""
|
||||
try:
|
||||
# SECURITY: Validate inputs to prevent path traversal attacks
|
||||
# Reject any path components that contain directory traversal sequences
|
||||
if ".." in folder_name or ".." in file_name:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Path traversal patterns ('..') are not allowed in folder or file names"
|
||||
)
|
||||
|
||||
# Only allow specific folder names (dynamic from config + package)
|
||||
allowed_folders = _get_allowed_profile_picture_folders(settings_service)
|
||||
if folder_name not in allowed_folders:
|
||||
raise HTTPException(status_code=400, detail=f"Folder must be one of: {', '.join(sorted(allowed_folders))}")
|
||||
|
||||
# Validate file name contains no path separators
|
||||
if "/" in file_name or "\\" in file_name:
|
||||
raise HTTPException(status_code=400, detail="File name cannot contain path separators ('/' or '\\')")
|
||||
|
||||
extension = file_name.split(".")[-1]
|
||||
config_dir = settings_service.settings.config_dir
|
||||
config_path = Path(config_dir) # type: ignore[arg-type]
|
||||
file_path = config_path / "profile_pictures" / folder_name / file_name
|
||||
config_path = Path(config_dir).resolve() # type: ignore[arg-type]
|
||||
|
||||
# Construct the file path
|
||||
file_path = (config_path / "profile_pictures" / folder_name / file_name).resolve()
|
||||
|
||||
# SECURITY: Verify the resolved path is still within the allowed directory
|
||||
# This prevents path traversal even if symbolic links are involved
|
||||
allowed_base = (config_path / "profile_pictures").resolve()
|
||||
if not str(file_path).startswith(str(allowed_base)):
|
||||
# Return 404 to prevent path traversal attempts from revealing system structure
|
||||
raise HTTPException(status_code=404, detail="Profile picture not found")
|
||||
|
||||
# Fallback to package bundled profile pictures if not found in config_dir
|
||||
if not file_path.exists():
|
||||
from langflow.initial_setup import setup
|
||||
|
||||
package_path = Path(setup.__file__).parent / "profile_pictures" / folder_name / file_name
|
||||
package_base = Path(setup.__file__).parent / "profile_pictures"
|
||||
package_path = (package_base / folder_name / file_name).resolve()
|
||||
|
||||
# SECURITY: Verify package path is also within allowed directory
|
||||
allowed_package_base = package_base.resolve()
|
||||
if not str(package_path).startswith(str(allowed_package_base)):
|
||||
# Return 404 to prevent path traversal attempts from revealing system structure
|
||||
raise HTTPException(status_code=404, detail="Profile picture not found")
|
||||
|
||||
if package_path.exists():
|
||||
file_path = package_path
|
||||
else:
|
||||
@ -177,29 +248,30 @@ async def list_profile_pictures(
|
||||
config_dir = settings_service.settings.config_dir
|
||||
config_path = Path(config_dir) # type: ignore[arg-type]
|
||||
|
||||
people_path = config_path / "profile_pictures" / "People"
|
||||
space_path = config_path / "profile_pictures" / "Space"
|
||||
# Build list for all allowed folders (dynamic)
|
||||
allowed_folders = _get_allowed_profile_picture_folders(settings_service)
|
||||
|
||||
# List files directly from local filesystem
|
||||
people = [f.name for f in people_path.iterdir() if f.is_file()] if people_path.exists() else []
|
||||
space = [f.name for f in space_path.iterdir() if f.is_file()] if space_path.exists() else []
|
||||
results: list[str] = []
|
||||
cfg_base = config_path / "profile_pictures"
|
||||
if cfg_base.exists():
|
||||
for folder in sorted(allowed_folders):
|
||||
p = cfg_base / folder
|
||||
if p.exists():
|
||||
results += [f"{folder}/{f.name}" for f in p.iterdir() if f.is_file()]
|
||||
|
||||
# Fallback to package bundled profile pictures if config_dir is empty
|
||||
if not people and not space:
|
||||
# Fallback to package if config_dir produced no results
|
||||
if not results:
|
||||
from langflow.initial_setup import setup
|
||||
|
||||
package_base = Path(setup.__file__).parent / "profile_pictures"
|
||||
people_path = package_base / "People"
|
||||
space_path = package_base / "Space"
|
||||
people = [f.name for f in people_path.iterdir() if f.is_file()] if people_path.exists() else []
|
||||
space = [f.name for f in space_path.iterdir() if f.is_file()] if space_path.exists() else []
|
||||
for folder in sorted(allowed_folders):
|
||||
p = package_base / folder
|
||||
if p.exists():
|
||||
results += [f"{folder}/{f.name}" for f in p.iterdir() if f.is_file()]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
files = [f"People/{i}" for i in people]
|
||||
files += [f"Space/{i}" for i in space]
|
||||
|
||||
return {"files": files}
|
||||
return {"files": results}
|
||||
|
||||
|
||||
@router.get("/list/{flow_id}")
|
||||
|
||||
@ -4,7 +4,7 @@ import json
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from lfx.base.models.unified_models import (
|
||||
get_model_provider_variable_mapping,
|
||||
get_model_providers,
|
||||
@ -13,6 +13,7 @@ from lfx.base.models.unified_models import (
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
from langflow.api.utils import CurrentActiveUser, DbSession
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.deps import get_variable_service
|
||||
from langflow.services.variable.constants import CREDENTIAL_TYPE, GENERIC_TYPE
|
||||
from langflow.services.variable.service import DatabaseVariableService
|
||||
@ -93,7 +94,7 @@ class ModelStatusUpdate(BaseModel):
|
||||
return v.strip()
|
||||
|
||||
|
||||
@router.get("/providers", status_code=200)
|
||||
@router.get("/providers", status_code=200, dependencies=[Depends(get_current_active_user)])
|
||||
async def list_model_providers() -> list[str]:
|
||||
"""Return available model providers."""
|
||||
return get_model_providers()
|
||||
|
||||
@ -22,7 +22,7 @@ from langflow.services.database.models.vertex_builds.model import VertexBuildMap
|
||||
router = APIRouter(prefix="/monitor", tags=["Monitor"])
|
||||
|
||||
|
||||
@router.get("/builds")
|
||||
@router.get("/builds", dependencies=[Depends(get_current_active_user)])
|
||||
async def get_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSession) -> VertexBuildMapModel:
|
||||
try:
|
||||
vertex_builds = await get_vertex_builds_by_flow_id(session, flow_id)
|
||||
@ -31,7 +31,7 @@ async def get_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSessio
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.delete("/builds", status_code=204)
|
||||
@router.delete("/builds", status_code=204, dependencies=[Depends(get_current_active_user)])
|
||||
async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSession) -> None:
|
||||
try:
|
||||
await delete_vertex_builds_by_flow_id(session, flow_id)
|
||||
|
||||
@ -26,8 +26,12 @@ router = APIRouter(tags=["Users"], prefix="/users")
|
||||
async def add_user(
|
||||
user: UserCreate,
|
||||
session: DbSession,
|
||||
current_user: Annotated[User, Depends(get_current_active_superuser)], # noqa: ARG001
|
||||
) -> User:
|
||||
"""Add a new user to the database."""
|
||||
"""Add a new user to the database.
|
||||
|
||||
Requires superuser authentication to prevent unauthorized account creation.
|
||||
"""
|
||||
new_user = User.model_validate(user, from_attributes=True)
|
||||
try:
|
||||
new_user.password = get_password_hash(user.password)
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from lfx.base.prompts.api_utils import process_prompt_template
|
||||
from lfx.custom.validate import validate_code
|
||||
from lfx.log.logger import logger
|
||||
|
||||
from langflow.api.utils import CurrentActiveUser
|
||||
from langflow.api.v1.base import Code, CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/validate", tags=["Validate"])
|
||||
|
||||
|
||||
@router.post("/code", status_code=200)
|
||||
async def post_validate_code(code: Code, _current_user: CurrentActiveUser) -> CodeValidationResponse:
|
||||
@router.post("/code", status_code=200, dependencies=[Depends(get_current_active_user)])
|
||||
async def post_validate_code(code: Code) -> CodeValidationResponse:
|
||||
try:
|
||||
errors = validate_code(code.code)
|
||||
return CodeValidationResponse(
|
||||
@ -23,8 +23,10 @@ async def post_validate_code(code: Code, _current_user: CurrentActiveUser) -> Co
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.post("/prompt", status_code=200)
|
||||
async def post_validate_prompt(prompt_request: ValidatePromptRequest) -> PromptValidationResponse:
|
||||
@router.post("/prompt", status_code=200, dependencies=[Depends(get_current_active_user)])
|
||||
async def post_validate_prompt(
|
||||
prompt_request: ValidatePromptRequest,
|
||||
) -> PromptValidationResponse:
|
||||
try:
|
||||
if not prompt_request.frontend_node:
|
||||
return PromptValidationResponse(
|
||||
|
||||
@ -98,7 +98,8 @@ async def fetch_file_object(file_id: uuid.UUID, current_user: CurrentActiveUser,
|
||||
|
||||
# Make sure the user has access to the file
|
||||
if file.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="You don't have access to this file")
|
||||
# Return 404 to prevent information disclosure about resource existence
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
return file
|
||||
|
||||
@ -225,8 +226,8 @@ async def upload_user_file(
|
||||
# S3 bucket doesn't exist or file not found, or file was uploaded but can't be found
|
||||
raise HTTPException(status_code=404, detail=str(e)) from e
|
||||
except PermissionError as e:
|
||||
# Access denied or invalid credentials
|
||||
raise HTTPException(status_code=403, detail=str(e)) from e
|
||||
# Access denied or invalid credentials - return 500 as this is a server config issue
|
||||
raise HTTPException(status_code=500, detail="Error accessing storage") from e
|
||||
except Exception as e:
|
||||
# General error saving file or getting file size
|
||||
raise HTTPException(status_code=500, detail=f"Error accessing file: {e}") from e
|
||||
|
||||
@ -3,10 +3,11 @@ from asyncio import to_thread
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
from langflow.logging import logger
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.deps import get_telemetry_service
|
||||
from langflow.services.telemetry.schema import EmailPayload
|
||||
|
||||
@ -135,7 +136,7 @@ async def _send_email_telemetry(email: str) -> None:
|
||||
logger.debug(f"Successfully sent email telemetry event: {payload.email}")
|
||||
|
||||
|
||||
@router.get("/")
|
||||
@router.get("/", dependencies=[Depends(get_current_active_user)])
|
||||
async def get_registration():
|
||||
"""Get the registered user (if any)."""
|
||||
try:
|
||||
|
||||
@ -21,8 +21,8 @@ async def test_get_version(client: AsyncClient):
|
||||
assert "package" in result, "The dictionary must contain a key called 'package'"
|
||||
|
||||
|
||||
async def test_get_config(client: AsyncClient):
|
||||
response = await client.get("api/v1/config")
|
||||
async def test_get_config(client: AsyncClient, logged_in_headers: dict):
|
||||
response = await client.get("api/v1/config", headers=logged_in_headers)
|
||||
result = response.json()
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@ -576,15 +576,17 @@ async def test_download_profile_picture_not_found_in_both_locations(empty_config
|
||||
|
||||
|
||||
async def test_download_profile_picture_invalid_folder(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test 404 when using an invalid folder name.
|
||||
"""Test 400 when using an invalid folder name.
|
||||
|
||||
Only 'People' and 'Space' folders should exist in the package.
|
||||
Only 'People' and 'Space' folders are whitelisted for security.
|
||||
Invalid folder names should be rejected with 400 Bad Request.
|
||||
"""
|
||||
response = await files_client.get("api/v1/files/profile_pictures/InvalidFolder/file.svg")
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == 400
|
||||
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
# Check for the new specific error message
|
||||
assert "folder must be one of" in data["detail"].lower()
|
||||
|
||||
|
||||
async def test_download_profile_picture_config_dir_takes_precedence(setup_profile_pictures, files_client):
|
||||
|
||||
@ -2,9 +2,9 @@ from fastapi import status
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
async def test_add_user(client: AsyncClient):
|
||||
async def test_add_user(client: AsyncClient, logged_in_headers_super_user):
|
||||
basic_case = {"username": "string", "password": "string"}
|
||||
response = await client.post("api/v1/users/", json=basic_case)
|
||||
response = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
|
||||
result = response.json()
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
@ -49,7 +49,7 @@ async def test_patch_user(client: AsyncClient, logged_in_headers_super_user):
|
||||
name = "string"
|
||||
updated_name = "string2"
|
||||
basic_case = {"username": name, "password": "string"}
|
||||
response_ = await client.post("api/v1/users/", json=basic_case)
|
||||
response_ = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
|
||||
id_ = response_.json()["id"]
|
||||
basic_case["username"] = updated_name
|
||||
response = await client.patch(f"api/v1/users/{id_}", json=basic_case, headers=logged_in_headers_super_user)
|
||||
@ -88,7 +88,7 @@ async def test_reset_password(client: AsyncClient, logged_in_headers, active_use
|
||||
|
||||
async def test_delete_user(client: AsyncClient, logged_in_headers_super_user):
|
||||
basic_case = {"username": "string", "password": "string"}
|
||||
response_ = await client.post("api/v1/users/", json=basic_case)
|
||||
response_ = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
|
||||
id_ = response_.json()["id"]
|
||||
response = await client.delete(f"api/v1/users/{id_}", headers=logged_in_headers_super_user)
|
||||
result = response.json()
|
||||
|
||||
@ -251,9 +251,14 @@ class TestAPIEndpoints:
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
# Override auth dependency to bypass authentication
|
||||
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/registration/") # Fixed: Added /registration prefix
|
||||
@ -271,9 +276,14 @@ class TestAPIEndpoints:
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
# Override auth dependency to bypass authentication
|
||||
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/registration/") # Fixed: Added /registration prefix
|
||||
@ -289,9 +299,14 @@ class TestAPIEndpoints:
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
# Override auth dependency to bypass authentication
|
||||
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/registration/") # Fixed: Added /registration prefix
|
||||
@ -328,6 +343,7 @@ class TestIntegration:
|
||||
"""Test API endpoints with actual file operations."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
|
||||
# Set up temporary file path
|
||||
test_file = tmp_path / "registration.json"
|
||||
@ -335,6 +351,10 @@ class TestIntegration:
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
# Override auth dependency to bypass authentication
|
||||
app.dependency_overrides[get_current_active_user] = lambda: MagicMock()
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# Test registration
|
||||
|
||||
@ -231,18 +231,19 @@ What is a good name for a company that makes {product}?
|
||||
INVALID_PROMPT = "This is an invalid prompt without any input variable."
|
||||
|
||||
|
||||
async def test_valid_prompt(client: AsyncClient):
|
||||
async def test_valid_prompt(client: AsyncClient, logged_in_headers):
|
||||
PROMPT_REQUEST["template"] = VALID_PROMPT
|
||||
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST)
|
||||
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["input_variables"] == ["product"]
|
||||
|
||||
|
||||
async def test_invalid_prompt(client: AsyncClient):
|
||||
async def test_invalid_prompt(client: AsyncClient, logged_in_headers):
|
||||
PROMPT_REQUEST["template"] = INVALID_PROMPT
|
||||
response = await client.post(
|
||||
"api/v1/validate/prompt",
|
||||
json=PROMPT_REQUEST,
|
||||
headers=logged_in_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["input_variables"] == []
|
||||
@ -257,9 +258,9 @@ async def test_invalid_prompt(client: AsyncClient):
|
||||
("{a}, {b}, and {c} are variables.", ["a", "b", "c"]),
|
||||
],
|
||||
)
|
||||
async def test_various_prompts(client, prompt, expected_input_variables):
|
||||
async def test_various_prompts(client, logged_in_headers, prompt, expected_input_variables):
|
||||
PROMPT_REQUEST["template"] = prompt
|
||||
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST)
|
||||
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["input_variables"] == expected_input_variables
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user