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
11 changes: 11 additions & 0 deletions airbyte/cloud/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from __future__ import annotations

import contextlib
import uuid
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
Expand Down Expand Up @@ -484,7 +485,17 @@ def get_connection(

This method does not fetch data from the API. It returns a `CloudConnection` object,
which will be loaded lazily as needed.

Raises:
AirbyteLibInputError: If `connection_id` is not a valid UUID.
"""
try:
uuid.UUID(connection_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Malformed connection IDs still reach Cloud

For a UUID with misplaced hyphens, uuid.UUID succeeds and get_connection accepts the malformed ID. CloudConnection keeps the original string, so API calls can still return HTTP 500.

Learn more

Python's UUID parser removes hyphens anywhere in the string before checking the hex digits. It also accepts braces and a URN prefix. This method ignores the parsed value and passes the original string to CloudConnection, which forwards it unchanged through _fetch_connection_info. The server can therefore receive a malformed path ID despite successful local validation.

Example: 1234-5678-1234-1234-1234-1234567890ab parses as a UUID, but the connection lookup still sends that exact, wrongly grouped ID.

Recommended fix: Require the connection ID to match the server's canonical UUID string format before constructing CloudConnection; compare the original ID with the parsed canonical representation if the server supports lowercase canonical IDs only, or explicitly account for accepted case variants.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

except ValueError:
raise exc.AirbyteLibInputError(
message="`connection_id` must be a UUID.",
context={"connection_id": connection_id},
) from None
return CloudConnection(
workspace=self,
connection_id=connection_id,
Expand Down
18 changes: 18 additions & 0 deletions tests/unit_tests/test_cloud_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ def _connection() -> CloudConnection:
return CloudConnection(workspace=workspace, connection_id="connection-id")


def test_get_connection_validates_connection_id_uuid() -> None:
"""`get_connection` rejects malformed IDs before any API call."""
workspace = CloudWorkspace(
workspace_id="workspace-id",
bearer_token="token",
api_root="https://api.airbyte.com/v1",
)

with pytest.raises(AirbyteLibInputError) as exc_info:
workspace.get_connection("not-a-uuid")

assert "`connection_id` must be a UUID." in str(exc_info.value)

connection_id = "12345678-1234-1234-1234-1234567890ab"
connection = workspace.get_connection(connection_id)
assert connection.connection_id == connection_id


def _scheduled_connection_response(*, basic: bool = False) -> models.ConnectionResponse:
"""Create a response with a real public API schedule model."""
return models.ConnectionResponse(
Expand Down
14 changes: 9 additions & 5 deletions tests/unit_tests/test_mcp_connection_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
def _connection_response() -> models.ConnectionResponse:
"""Create a complete SDK response for the network boundary."""
return models.ConnectionResponse(
connection_id="connection-id",
connection_id="12345678-1234-1234-1234-1234567890ab",
created_at=0,
destination_id="destination-id",
name="name",
Expand Down Expand Up @@ -70,7 +70,10 @@ def schedule_backend(
with responses.RequestsMock(assert_all_requests_are_fired=False) as http:
http.post(
"https://config.example/custom/web_backend/connections/update",
json={"connectionId": "connection-id", "scheduleType": "basic"},
json={
"connectionId": "12345678-1234-1234-1234-1234567890ab",
"scheduleType": "basic",
},
)
http.post("https://api.segment.io/v1/batch", json={"success": True})
if request.param:
Expand All @@ -93,7 +96,8 @@ async def _update(server: FastMCP, **arguments: object) -> str:
"""Call the registered tool through the serialized MCP boundary."""
async with Client(server) as client:
result = await client.call_tool(
"update_cloud_connection", {"connection_id": "connection-id", **arguments}
"update_cloud_connection",
{"connection_id": "12345678-1234-1234-1234-1234567890ab", **arguments},
)
return str(result)

Expand Down Expand Up @@ -150,7 +154,7 @@ def test_direct_update_rejects_invalid_intervals_before_status_change(
with pytest.raises(AirbyteLibInputError, match="positive whole number"):
cloud_mcp.update_cloud_connection(
ctx=None,
connection_id="connection-id",
connection_id="12345678-1234-1234-1234-1234567890ab",
enabled=True,
interval_hours=interval_hours,
cron_expression=None,
Expand Down Expand Up @@ -189,7 +193,7 @@ def test_update_interval_schedule_serialized(
"https://config.example/custom/web_backend/connections/update"
)
assert json.loads(calls[0].request.body) == {
"connectionId": "connection-id",
"connectionId": "12345678-1234-1234-1234-1234567890ab",
"scheduleType": "basic",
"scheduleData": {"basicSchedule": {"timeUnit": "hours", "units": 24}},
"skipReset": True,
Expand Down
Loading